Kotlin、SpringBoot 和 Mockk 的 POST 方法出错
Error in POST method with Kotlin, SpringBoot and Mockk
当我使用 data class 和 日期字段(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 就可以)
这是我使用的 classes:
文件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(这个class就是问题所在)
@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 方法
文件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 库中,因为从测试我可以到达端点并正确打印值
print from endpoint: print("person: $person") //this line is in the endpoint
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: 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)]) on
PersonRepository(#1).saveAll([Person(id=0, name=string,
lastName=string, nickName=string, hireDate=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: 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)])
错误因修复而异,我也得到了
JSON parse error: Cannot deserialize value of type java.time.LocalDate
from ...
... 48 more
但是在 Spring 中使用 Kotlin
序列化 LocalDate 总是同样的问题
如能提供任何帮助,我们将不胜感激。
在阅读了很多针对此问题的可能解决方案之后,我找到了一些解决此问题的方法 "issue"。
就像我写的那样,我使用的是 Gson,所以,我已经为 序列化 实现了一个重写,另一个为 反序列化 实现了重写LocalDates,我还发现了一个 hack(?) 覆盖了 Data class 中的 ToString() 方法,更重要的是,当我尝试 deserialize a post LocalDate 字段中有空值的响应,我还想(再次)说,问题出在 不在生产代码中测试,让我们看看:
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 方法用 endDate
中的空值覆盖数据 class 中的 ToString
a) 修改数据class
@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) 测试从数据 class
实现 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
}
我希望这个解决方法有用。
当我使用 data class 和 日期字段(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 就可以)
这是我使用的 classes:
文件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(这个class就是问题所在)
@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 方法
文件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 库中,因为从测试我可以到达端点并正确打印值
print from endpoint: print("person: $person") //this line is in the endpoint
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: 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)]) on PersonRepository(#1).saveAll([Person(id=0, name=string, lastName=string, nickName=string, hireDate=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: 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)])
错误因修复而异,我也得到了
JSON parse error: Cannot deserialize value of type
java.time.LocalDate
from ... ... 48 more
但是在 Spring 中使用 Kotlin
序列化 LocalDate 总是同样的问题如能提供任何帮助,我们将不胜感激。
在阅读了很多针对此问题的可能解决方案之后,我找到了一些解决此问题的方法 "issue"。
就像我写的那样,我使用的是 Gson,所以,我已经为 序列化 实现了一个重写,另一个为 反序列化 实现了重写LocalDates,我还发现了一个 hack(?) 覆盖了 Data class 中的 ToString() 方法,更重要的是,当我尝试 deserialize a post LocalDate 字段中有空值的响应,我还想(再次)说,问题出在 不在生产代码中测试,让我们看看:
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 方法用 endDate
中的空值覆盖数据 class 中的 ToStringa) 修改数据class
@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) 测试从数据 class
实现 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
}
我希望这个解决方法有用。