Ошибка в методе POST с Kotlin, SpringBoot и Mockk - PullRequest
0 голосов
/ 02 апреля 2020

У меня проблема с тестом (макет) типа POST в kotlin, когда я использую класс данных с полем date (LocalDate).

Это стек, который я использую:

springBoot      : v2.1.7.RELEASE
Java            : jdk-11.0.4
kotlinVersion   : '1.3.70'
junitVersion    : '5.6.0'
junit4Version   : '4.13'
mockitoVersion  : '3.2.4'
springmockk     : '1.1.3'

Когда я выполняю метод POST в приложении, все в порядке, у меня есть ответ, и данные правильно сохраняются в БД:

curl -X POST "http://127.0.1.1:8080/v1/person/create" -H  "accept: */*" -H  "Content-Type: application/json" -d "[  {    \"available\": true,    \"endDate\": \"2090-01-02\",    \"hireDate\": \"2020-01-01\",    \"id\": 0,    \"lastName\": \"stringTest\",    \"name\": \"stringTest\",    \"nickName\": \"stringTest\"  }]"

Но когда я пытаюсь выполнить тест метода POST, я не могу (только с методом POST, с GET все в порядке)

Это классы, которые я использую:

Файл Person.kt

@Entity
data class Person(
            @Id @Column(name = "id") @GeneratedValue(strategy = GenerationType.AUTO)
            var id: Long,

            var name: String,
            var lastName: String,
            var nickName: String,
            @JsonFormat(pattern = "yyyy-MM-dd")
            var hireDate: LocalDate,
            @JsonFormat(pattern = "yyyy-MM-dd")
            var endDate: LocalDate,
            var available: Boolean
            ) {
            constructor()  : this(0L, "Name example",
                    "LastName example",
                    "Nick example",
                    LocalDate.of(2020,1,1),
                    LocalDate.of(2090,1,1),
                    true)

Файл PersonService.kt

@Service
class PersonService(private val personRepository: PersonRepository) {

    fun findAll(): List<Person> {
        return personRepository.findAll()
    }

    fun saveAll(personList: List<Person>): MutableList<person>? {
        return personRepository.saveAll(personList)
    }
}

Файл PersonApi.kt

@RestController
@RequestMapping("/v1/person/")
class PersonApi(private val personRepository: PersonRepository) {

    @Autowired
    private var personService = PersonService(personRepository)

    @PostMapping("create")
    fun createPerson(@Valid
                     @RequestBody person: List<Person>): ResponseEntity<MutableList<Person>?> {

        print("person: $person") //this is only for debug purpose only
        return ResponseEntity(personService.saveAll(person), HttpStatus.CREATED)
    }
}

И, наконец,

PersonApiShould.kt (этот класс является проблемой)

@EnableAutoConfiguration
@AutoConfigureMockMvc
@ExtendWith(MockKExtension::class)
internal class PersonApiShould {

    private lateinit var gsonBuilder: GsonBuilder
    private lateinit var gson: Gson
    lateinit var mockMvc: MockMvc

    @MockkBean
    lateinit var personService: PersonService

    @BeforeEach
    fun setUp() {
        val repository = mockk<PersonRepository>()
        personService = PersonService(repository)
        mockMvc = standaloneSetup(PersonApi(repository)).build()

        gson = GsonBuilder()
                .registerTypeAdapter(Person::class.java, PersonDeserializer())
                .create()
        gsonBuilder = GsonBuilder()
    }

    @AfterEach
    fun clear() {
        clearAllMocks()}

    @Test
    fun `create person`() {

         val newPerson = Person(1L, 
                "string",    //name
                "string",    //lastName   
                "string",    //nickname
                LocalDate.of(2020, 1, 1),    //hireDate
                LocalDate.of(2090, 1, 2),    //endDate
                true)    //available
        val contentList = mutableListOf<Person>()
        contentList.add(newPerson)

//        also tried with
//        every { personService.findAll() }.returns(listOf<Person>())
//        every { personService.saveAll(mutableListOf<Person>())}.returns(Person())

        every { personService.findAll() }.returns(contentList)
        every { personService.saveAll(any()) }.returns(contentList)


/*    didn't work either
       val personJson = gsonBuilder.registerTypeAdapter(Date::class.java, DateDeserializer())
                .create().toJson(newPerson)
*/

        val content = "[\n" +
                "  {\n" +
                "    \"available\": true,\n" +
                "    \"endDate\": \"2090-01-02\",\n" +
                "    \"hireDate\": \"2020-01-01\",\n" +
                "    \"id\": 0,\n" +
                "    \"lastName\": \"string\",\n" +
                "    \"name\": \"string\",\n" +
                "    \"nickName\": \"string\"\n" +
                "  }\n" +
                "]"

        val httpResponse = mockMvc.perform(post("/v1/resto/person/create")
                .content(content)  //also tried with .content(contentList)
                .contentType(MediaType.APPLICATION_JSON))
                .andReturn()

        // error, because, httpResponse is always empty
        val personCreated: List<Person> = gson.fromJson(httpResponse.response.contentAsString,
                object : TypeToken<List<Person>>() {}.type)

        assertEquals(newPerson.name, personCreated.get(0).name)
    }

У Gson есть некоторые проблемы при десериализации дат, это парсер (хак), он работает для моего метода GET

File PersonDeserializer.kt

class PersonDeserializer : JsonDeserializer<Person> {

    override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Person {
        json as JsonObject

        val name = json.get("name").asString
        val lastName = json.get("lastName").asString
        val nickName = json.get("nickName").asString
        val available = json.get("available").asBoolean

        val hireDate = LocalDate.of((json.get("hireDate") as JsonArray).get(0).asInt,
                (json.get("hireDate") as JsonArray).get(1).asInt,
                (json.get("hireDate") as JsonArray).get(2).asInt)

        val endDate = LocalDate.of((json.get("endDate") as JsonArray).get(0).asInt,
                (json.get("endDate") as JsonArray).get(1).asInt,
                (json.get("endDate") as JsonArray).get(2).asInt)

        return Person(1L, name, lastName, nickName, hireDate, endDate, available)
    }
}

Я вижу, что ошибка в MOCKK Library, потому что из теста я могу добраться до конечной точки и правильно напечатайте значение

печать с конечной точки: print ("person: $ person") // эта строка находится в конечной точке

Person: [Person(id=0, name=string, lastName=string, nickName=string, hireDate=2020-01-01, endDate=2090-01-02, available=true)]

Журнал ошибок тестирования

19: 27: 24,840 [main] DEBUG io.mockk.impl.recording.states.AnsweringState - Throwing io.mockk.MockKException: не найден ответ для: PersonRepository (# 1) .saveAll ([Person (id = 0, name = string, lastName = string, nickName = string, hireDate = 2020-01-01, endDate = 2090-01-02, available = true)]) в PersonRepository (# 1) .saveAll ([Person (id = 0, name = string, lastName = string, nickName = string, hireDate = 2020-01-01, endDate = 2090-01-02, доступно = true)])

19: 27: 24.844 [main] DEBUG org.springframework.test.web.servlet.TestDispatcherServlet - не удалось полный запрос: io.mockk.MockKException: не найден ответ для: PersonRepository (# 1) .saveAll ([Person (id = 0, имя = строка, l astName = string, nickName = string, hireDate = 2020-01-01, endDate = 2090-01-02, доступно = true)])

org.springframework.web .util.NestedServletException: запрос обработка не удалась; вложенное исключение: io.mockk.MockKException: нет найден ответ для: PersonRepository (# 1) .saveAll ([Person (id = 0, name = string, lastName = string, nickName = строка, hireDate = 2020-01-01, endDate = 2090-01-02, доступно = true)])

Ошибки варьируются в зависимости от исправления, также я получил

JSON ошибка разбора: Невозможно десериализовать значение типа java.time.LocalDate из ... ... еще 48

Но всегда та же проблема с сериализацией LocalDate весной с Kotlin

Любая помощь, которую вы можете предоставить, будет принята с благодарностью.

1 Ответ

0 голосов
/ 06 мая 2020

После прочтения множества возможных решений этой проблемы я нашел несколько обходных путей для решения этой "проблемы".

Как я уже писал, я использую Gson, поэтому я реализовал переопределение для сериализация и другое для десериализации LocalDates, также я обнаружил взлом (?), Который переопределяет ToString () метод в классе данных, и что более важно, я нашел больше проблем, когда я пытался десериализовать пост-ответ с нулями в поле LocalDate , также я хотел бы сказать (снова), что проблема была в TEST NOT В производственном коде , давайте посмотрим:

1) Простой метод Get, без нулевых значений

    @Test
    fun `return all non active persons`() {
        val personList = givenAListOfpersons()

        val activepersonsCount: Int = personList.filter { person ->
            person.available==false }.size //2

        every { personservice.findActivePersons() } returns personList

        val httpResponse = mockMvc.perform(get("/v1/resto/person/list?available={available}", "false")
                .param("available", "false")
                .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk)
                .andExpect(jsonPath("$", hasSize<Any>(activepersonsCount)))
                .andReturn()

// Note: Simple deserialization: explain later

        val response: List<person> = gsonDeserializer.fromJson(httpResponse.response.contentAsString,
                object : TypeToken<List<person>>() {}.type)


        assertEquals(personList.get(0).name, response.get(0).name)
        assertEquals(personList.get(0).lastName, response.get(0).lastName)
        assertEquals(personList.get(0).nickName, response.get(0).nickName)
        assertEquals(personList.get(0).hireDate, response.get(0).hireDate)
        assertEquals(personList.get(0).available, response.get(0).available)
    }

2) Переопределение метода Post ToString в классе данных с нулевыми значениями в endDate

a) Изменить класс данных

@Entity
data class person(
        @Id @Column(name = "id") @GeneratedValue(strategy = GenerationType.AUTO)
        var id: Long,

        var name: String,
        var lastName: String,
        var nickName: String,
        @JsonFormat(pattern = "yyyy-MM-dd")
        var hireDate: LocalDate,

        @JsonFormat(pattern = "yyyy-MM-dd")
        var endDate: LocalDate?, //note this

        var available: Boolean
        ) {
        constructor()  : this(0L, "xx",
                "xx",
                "xx",
                LocalDate.of(2020,1,1),
                null,
                true)

        //here
        override fun toString(): String {
                return  "["+"{"+
                        '\"' +"id"+'\"'+":" + id +
                        ","+ '\"' +"name"+'\"'+":"+ '\"' + name + '\"' +
                        ","+ '\"' +"lastName"+'\"'+":"+ '\"' + lastName + '\"' +
                        ","+ '\"' +"nickName"+'\"'+":"+ '\"' + nickName + '\"' +
                        ","+ '\"' +"hireDate"+'\"'+":"+ '\"' + hireDate + '\"' +
                        ","+ '\"' +"endDate"+'\"'+":"+ '\"' + endDate + '\"' +
                        ","+ '\"' +"available"+'\"'+":" + available +
                        "}"+"]";
        }
}

b) Тестировать реализацию toString () из Data класс

@Test
    fun `create person`() {

        val personList = givenAListOfpersons() as MutableList<person>


        every { personService.saveAll(any()) }.returns(personList)

        val httpPostResponse = mockMvc.perform(post("/v1/resto/person/create")
                .contentType(MediaType.APPLICATION_JSON)
                .content(personTest.toString()))  //THIS
                .andDo(print())
                .andExpect(status().isCreated) //It´s works!!
                .andReturn()

        // Note the gsonDeserializer, explain later
        val personDeserializerToList = gsonDeserializer.fromJson<List<person>>(httpPostResponse.response.contentAsString,
                object : TypeToken<List<person>>() {}.rawType).get(0) as LinkedTreeMap<String, Object>

        assertEquals(personList.get(0).name, personDeserializerToList["name"])
        assertEquals(personList.get(0).lastName, personDeserializerToList["lastName"])
        assertEquals(personList.get(0).nickName, personDeserializerToList["nickName"])
        assertEquals(personList.get(0).hireDate, personDeserializerToList["hireDate"]))

        assertNull(personDeserializerToList["endDate"]))

        assertEquals(personList.get(0).available, personDeserializerToList["available"])
    }

3) Рекомендуемый способ: использование переопределения Gson. Сериализация. d Формат LocalDates:

    @Test
    fun `create person`() {

        val personList = givenAListOfPersons() as MutableList<Person

        // It´s work´s
        val personSerializerToString = gsonSerializer.toJson(personList, object : TypeToken<List<person>>() {}.type)

        every { personService.saveAll(any()) }.returns(personList)

        val httpPostResponse = mockMvc.perform(post("/v1/resto/person/create")
                .contentType(MediaType.APPLICATION_JSON)
                .content(personSerializerToString))
                .andDo(print())
                .andExpect(status().isCreated) //It´s Work´s!
                .andReturn()

// Deserialization problem: endDate is null, and we cant parse a null in Gson
// that´s why i use **rawType**
        val personDeserializerToList = gsonDeserializer.fromJson<List<person>>(httpPostResponse.response.contentAsString,
                object : TypeToken<List<person>>() {}.rawType).get(0) as LinkedTreeMap<String, Object>

        assertEquals(personList.get(0).name, personDeserializerToList["name"])
        assertEquals(personList.get(0).lastName, personDeserializerToList["lastName"])
        assertEquals(personList.get(0).nickName, personDeserializerToList["nickName"])

// Note formatToLocalDate method: The date i receive from post is 
// in this format ==>  **[2020.0,1.0,1.0]** so i must to parse this 
// format to LocalDate

        assertEquals(personList.get(0).hireDate, formatToLocalDate(personDeserializerToList["hireDate"])) 

        assertNull(personDeserializerToList["endDate"])

        assertEquals(personList.get(0).available, personDeserializerToList["available"])
    }

Наконец, сериализация, десериализация и formatToLocalDate:

a) Сначала мы должны установить конфигурации:

@ExtendWith(MockKExtension::class)
@EnableAutoConfiguration
@AutoConfigureMockMvc
internal class PersonApiShould {

    private lateinit var gsonSerializer: Gson
    private lateinit var gsonDeserializer: Gson

    lateinit var mockMvc: MockMvc

    @MockkBean
    lateinit var personService: PersonService

    @BeforeEach
    fun setUp() {
        val repository = mockk<PersonRepository>()
        personService = PersonService(repository)
        mockMvc = standaloneSetup(PersonApi(repository)).build()


        // Note this
        gsonDeserializer = GsonBuilder()
                .registerTypeAdapter(Person::class.java, PersonDeserializer())
                .create()

        gsonSerializer = GsonBuilder()
                .registerTypeAdapter(Person::class.java, PersonSerializer())
                .create()
    }

    @AfterEach
    fun clear() {
        clearAllMocks()
    }
tests ...

b) И методы

// This is because i receive [2020.0,1.0,1.0]
private fun formatToLocalDate(dates: Object?): LocalDate? {
    return LocalDate.of(
            ((dates as ArrayList<Object>).get(0) as Double).toInt(),
            ((dates as ArrayList<Object>).get(1) as Double).toInt(),
            ((dates as ArrayList<Object>).get(2) as Double).toInt())
}
//Gson have some issues when deserialize dates, this is a parser (hack)
// This parser have some troubles handling null values, that´s why i use rawType instead, 
//otherwise use this method

//Context: If we try to cast nulls in this class, we are going to receive this kind 
// of errors 
// ERROR with nulls:
//java.lang.ClassCastException: class com.google.gson.JsonNull cannot be cast to 
//class 
//com.google.gson.JsonArray (com.google.gson.JsonNull and 
//com.google.gson.JsonArray are in unnamed module of loader 'app')


class PersonDeserializer : JsonDeserializer<Person?> {

    override fun deserialize(jsonPersonResponse: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Person? {
        jsonPersonResponse as JsonObject

        val name = jsonPersonResponse.get("name").asString
        val lastName = jsonPersonResponse.get("lastName").asString
        val nickName = jsonPersonResponse.get("nickName").asString
        val available = jsonPersonResponse.get("available").asBoolean

        val hireDate = LocalDate.of((jsonPersonResponse.get("hireDate") as JsonArray).get(0).asInt,
                (jsonPersonResponse.get("hireDate") as JsonArray).get(1).asInt,
                (jsonPersonResponse.get("hireDate") as JsonArray).get(2).asInt)

        // remember, this Gson, cant handle null values and endDate is usually null 
        val endDate = LocalDate.of((jsonPersonResponse.get("endDate") as JsonArray).get(0).asInt,
                (jsonPersonResponse.get("endDate") as JsonArray).get(1).asInt,
                (jsonPersonResponse.get("endDate") as JsonArray).get(2).asInt)

        return Person(1L, name, lastName, nickName, hireDate, endDate, available)
    }
}
//Gson have some issues when serializing dates, this is a parser (hack)
class PersonSerializer : JsonSerializer<Person> {
    override fun serialize(src: Person, typeOfSrc: Type?, context: JsonSerializationContext): JsonObject {
        val PersonJson = JsonObject()
        PersonJson.addProperty("id", src.id.toInt())
        PersonJson.addProperty("name", src.name)
        PersonJson.addProperty("lastName", src.lastName)
        PersonJson.addProperty("nickName", src.nickName)
        PersonJson.addProperty("hireDate", src.hireDate.toString())

        if (src.endDate != null) {
            PersonJson.addProperty("endDate", src.endDate.toString())
        } else {
            PersonJson.addProperty("endDate", "".toShortOrNull())
        }

        PersonJson.addProperty("available", src.available)
        return PersonJson
    }

Я надеюсь, что этот обходной путь может быть полезным.

...