问题描述
当我在 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方法出错的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!