本文介绍了Kotlin,SpringBoot和Mockk的POST方法出错的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

当我在 date 字段(LocalDate)中使用数据类时,我在kotlin中遇到了POST类型的测试(模拟)问题.

I have a problem with a test (mock) of type POST in kotlin, when i use a data class with a date field (LocalDate).

这是Stack im的使用方式:

This is the Stack im using:

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方法时,一切正常,我有响应,并且数据已正确保存在db中:

When i execute the POST method in the app, all is ok, i have the response and the data is saved correctly in the db:

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可以)

But when i try to make the test of the POST Method, i cant (only with POST method, with GET is ok)

这是我使用的类:

文件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在反序列化日期时遇到一些问题,这是一个解析器(hack),适用于我的GET方法

Gson have some issues when deserialize dates, this is a parser (hack), it works for my GET method

文件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库中,因为从测试中我可以到达端点并正确打印该值

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

错误测试日志

19:27:24.844 [main]调试 org.springframework.test.web.servlet.TestDispatcherServlet-无法 完整的请求:io.mockk.MockKException:未找到以下答案: PersonRepository(#1).saveAll([Person(id = 0,name = string, lastName =字符串,nickName =字符串,hirateDate = 2020-01-01, endDate = 2090-01-02,available = true)])

19:27:24.844 [main] DEBUG org.springframework.test.web.servlet.TestDispatcherServlet - Failed to complete request: io.mockk.MockKException: no answer found for: PersonRepository(#1).saveAll([Person(id=0, name=string, lastName=string, nickName=string, hireDate=2020-01-01, endDate=2090-01-02, available=true)])

org.springframework.web.util.NestedServletException:请求 处理失败;嵌套异常是io.mockk.MockKException:否 找到以下答案:PersonRepository(#1).saveAll([Person(id = 0, 名称=字符串,姓氏=字符串,昵称=字符串,hirateDate = 2020-01-01, endDate = 2090-01-02,available = true)])

org.springframework.web.util.NestedServletException: Request processing failed; nested exception is io.mockk.MockKException: no answer found for: PersonRepository(#1).saveAll([Person(id=0, name=string, lastName=string, nickName=string, hireDate=2020-01-01, endDate=2090-01-02, available=true)])

错误因修复而异,我也得到了

Errors varies, depending the fix, also i got

但是使用Kotlin在Spring中对LocalDate进行序列化总是一个相同的问题

But always is the same problem with serialization of LocalDate in Spring with Kotlin

我们将不胜感激.

推荐答案

在阅读了很多有关此问题的可行解决方案之后,我发现了一些解决此问题"的解决方法.

After read a lot of posible solutions to this problem, i found some workarounds to handle this "issue".

就像我使用Gson所写的那样,因此,我对LocalDates的序列化反序列化进行了改写,我也发现了一个hack( ?)覆盖了Data类中的 ToString()方法,更重要的是,当我尝试对LocalDate中的空值进行反序列化 后响应时,我发现了更多问题字段,我还要(再次)要说,问题出在测试不在生产代码中,让我们看看:

Like i wrote, im using Gson, so, i´ve implemented an overrride for the serialization and another for the deserialization of LocalDates, also i found a hack(?) that override ToString() method in Data class, and more important, i found more issues when i tried to deserialize a post response with nulls in a LocalDate field, also i would like to say (again), that the problem were in the TEST NOT IN PRODUCTIVE CODE, let´s see:

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)在endDate中使用空值的数据类中的Post方法覆盖ToString

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()

@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替代Serialize方法并设置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
    }

我希望这种解决方法会有所帮助.

I hope this workaround could be useful.

这篇关于Kotlin,SpringBoot和Mockk的POST方法出错的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!

10-20 17:54