问题描述
我在Kotlin中编写了一个非常简单且通用的CRUD.我想做基本测试,如测试发布,删除,获取和放置.
I coded a very simple and common CRUD in Kotlin. I want to do basic tests as testing post, delete, get and put.
可能我理解错了:我使用Beforeeach的目的是插入一个寄存器,以便可以在get测试期间进行检查.我没有收到异常,但似乎在进行get测试时,如果波纹管测试中的其他id不同于1,则它总是NOT_FOUND时总是返回ok.
Probably I understood something wrong: I used Beforeeach aimed to insert a register so I could check during get test. I don't get exception but it seems during get test it always returning ok when it should be NOT_FOUND for any other id different than 1 in bellow test.
即使根据我的目的(简单的CRUD测试)看到其他不好的做法,任何朝着正确方向的线索或指导都会很受欢迎.
Any clue or guidance in right direction will be wellcome even if see other bad practice bellow based on my purpose (simple CRUD test).
测试
package com.mycomp.jokenpo
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.mycomp.jokenpo.controller.UserController
import com.mycomp.jokenpo.model.User
import com.mycomp.jokenpo.respository.UserRepository
import com.mycomp.jokenpo.service.UserService
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.mockito.InjectMocks
import org.mockito.Mock
import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.web.client.TestRestTemplate
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter
import org.springframework.test.context.junit.jupiter.SpringExtension
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders
import org.springframework.test.web.servlet.result.MockMvcResultHandlers
import org.springframework.test.web.servlet.result.MockMvcResultMatchers
import org.springframework.test.web.servlet.setup.MockMvcBuilders
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ExtendWith(SpringExtension::class)
@AutoConfigureMockMvc
class JokenpoApplicationTests {
@Autowired
lateinit var testRestTemplate: TestRestTemplate
@Autowired
private lateinit var mvc: MockMvc
@InjectMocks
lateinit var controller: UserController
@Mock
lateinit var respository: UserRepository
@Mock
lateinit var service: UserService
//private fun <T> any(type: Class<T>): T = Mockito.any<T>(type)
@BeforeEach
fun setup() {
MockitoAnnotations.initMocks(this)
mvc = MockMvcBuilders.standaloneSetup(controller).setMessageConverters(MappingJackson2HttpMessageConverter()).build()
`when`(respository.save(User(1, "Test")))
.thenReturn(User(1, "Test"))
}
@Test
fun createUser() {
//val created = MockMvcResultMatchers.status().isCreated
var user = User(2, "Test")
var jsonData = jacksonObjectMapper().writeValueAsString(user)
mvc.perform(MockMvcRequestBuilders.post("/users/")
.contentType(MediaType.APPLICATION_JSON)
.content(jsonData))
.andExpect(MockMvcResultMatchers.status().isOk)
//.andExpect(created)
.andDo(MockMvcResultHandlers.print())
.andReturn()
}
@Test
fun findUser() {
val ok = MockMvcResultMatchers.status().isOk
val builder = MockMvcRequestBuilders.get("/users?id=99") //no matther which id I type here it returns ok. I would expect only return for 1 based on my @BeforeEach
this.mvc.perform(builder)
.andExpect(ok)
}
}
控制器
package com.mycomp.jokenpo.controller
import com.mycomp.jokenpo.model.User
import com.mycomp.jokenpo.respository.UserRepository
import com.mycomp.jokenpo.service.UserService
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
import java.util.concurrent.atomic.AtomicLong
import javax.validation.Valid
@RestController
@RequestMapping("users")
class UserController (private val userService: UserService, private val userRepository: UserRepository){
val counter = AtomicLong()
// @GetMapping("/user")
// fun getUser(@RequestParam(value = "name", defaultValue = "World") name: String) =
// User(counter.incrementAndGet(), "Hello, $name")
@GetMapping()
fun getAllUsers(): List<User> =
userService.all()
@PostMapping
fun add(@Valid @RequestBody user: User): ResponseEntity<User> {
//user.id?.let { userService.save(it) }
val savedUser = userService.save(user)
return ResponseEntity.ok(savedUser)
}
@GetMapping("/{id}")
fun getUserById(@PathVariable(value = "id") userId: Long): ResponseEntity<User> {
return userRepository.findById(userId).map { user ->
ResponseEntity.ok(user)
}.orElse(ResponseEntity.notFound().build())
}
@DeleteMapping("/{id}")
fun deleteUserById(@PathVariable(value = "id") userId: Long): ResponseEntity<Void> {
return userRepository.findById(userId).map { user ->
userRepository.deleteById(user.id)
ResponseEntity<Void>(HttpStatus.OK)
}.orElse(ResponseEntity.notFound().build())
}
// @DeleteMapping("{id}")
// fun deleteUserById(@PathVariable id: Long): ResponseEntity<Unit> {
// if (noteService.existsById(id)) {
// noteService.deleteById(id)
// return ResponseEntity.ok().build()
// }
// return ResponseEntity.notFound().build()
// }
/////
// @PutMapping("{id}")
// fun alter(@PathVariable id: Long, @RequestBody user: User): ResponseEntity<User> {
// return userRepository.findById(userId).map { user ->
// userRepository. deleteById(user.id)
// ResponseEntity<Void>(HttpStatus.OK)
// }.orElse(ResponseEntity.notFound().build())
// }
}
存储库
package com.mycomp.jokenpo.respository
import com.mycomp.jokenpo.model.User
import org.springframework.data.repository.CrudRepository
interface UserRepository : CrudRepository<User, Long>
型号
package com.mycomp.jokenpo.model
import javax.persistence.*
@Entity
data class User(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long,
@Column(nullable = false)
val name: String
)
渐变依赖项
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("org.springframework.boot") version "2.2.6.RELEASE"
id("io.spring.dependency-management") version "1.0.9.RELEASE"
kotlin("jvm") version "1.3.71"
kotlin("plugin.spring") version "1.3.71"
kotlin("plugin.jpa") version "1.3.71"
}
group = "com.mycomp"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_1_8
val developmentOnly by configurations.creating
configurations {
runtimeClasspath {
extendsFrom(developmentOnly)
}
}
repositories {
mavenCentral()
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
developmentOnly("org.springframework.boot:spring-boot-devtools")
runtimeOnly("com.h2database:h2")
//runtimeOnly("org.hsqldb:hsqldb")
testImplementation("org.springframework.boot:spring-boot-starter-test") {
exclude(group = "org.junit.vintage", module = "junit-vintage-engine")
}
testImplementation ("com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0")
}
tasks.withType<Test> {
useJUnitPlatform()
}
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs = listOf("-Xjsr305=strict")
jvmTarget = "1.8"
}
}
application.yml
application.yml
spring:
datasource:
url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
username: sa
password:
driver-class-name: org.h2.Driver
platform: h2
h2:
console:
enabled: true
path: /h2-console #jdbc:h2:mem:testdb
如果有用,可以从 https://github.com/jimisdrpc/games下载整个项目,但我有信心上面的所有文件足以说明我的问题.
In case it is usefull the whole project can be dowloaded from https://github.com/jimisdrpc/games but I am confident that all files above are enough to ilustrate my issue.
推荐答案
为解决您的问题,我建议使用 @ MockBean ,可用于向Spring ApplicationContext
添加模拟的注释.
To solve your problem I suggest using @MockBean, an annotation that can be used to add mocks to a Spring ApplicationContext
.
我将按照以下方式重新编写测试(注意,我正在利用 mockito-kotlin 已经成为您项目的测试依赖项):
I would re-write your test as follows (notice that I'm taking advantage of mockito-kotlin already being a test dependency of your project):
package com.mycomp.jokenpo
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.mycomp.jokenpo.model.User
import com.mycomp.jokenpo.respository.UserRepository
import com.nhaarman.mockitokotlin2.whenever
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.junit.jupiter.api.extension.ExtendWith
import org.mockito.junit.jupiter.MockitoExtension
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.mock.mockito.MockBean
import org.springframework.http.MediaType
import org.springframework.test.context.junit.jupiter.SpringExtension
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.get
import org.springframework.test.web.servlet.post
import org.springframework.web.util.NestedServletException
@AutoConfigureMockMvc. // auto-magically configures and enables an instance of MockMvc
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
// Why configure Mockito manually when a JUnit 5 test extension already exists for that very purpose?
@ExtendWith(SpringExtension::class, MockitoExtension::class)
class JokenpoApplicationTests {
@Autowired
private lateinit var mockMvc: MockMvc
@MockBean
lateinit var respository: UserRepository
@BeforeEach
fun setup() {
// use mockito-kotlin for a more idiomatic way of setting up your test expectations
whenever(respository.save(User(1, "Test"))).thenAnswer {
it.arguments.first()
}
}
@Test
fun `Test createUser in the happy path scenario`() {
val user = User(1, "Test")
mockMvc.post("/users/") {
contentType = MediaType.APPLICATION_JSON
content = jacksonObjectMapper().writeValueAsString(user)
accept = MediaType.APPLICATION_JSON
}.andExpect {
status { isOk }
content { contentType(MediaType.APPLICATION_JSON) }
content { json("""{"id":1,"name":"Test"}""") }
}
verify(respository, times(1)).save(user)
}
@Test
fun `Test negative scenario of createUser`() {
val user = User(2, "Test")
assertThrows<NestedServletException> {
mockMvc.post("/users/") {
contentType = MediaType.APPLICATION_JSON
content = jacksonObjectMapper().writeValueAsString(user)
accept = MediaType.APPLICATION_JSON
}
}
verify(respository, times(1)).save(user)
}
@Test
fun findUser() {
mockMvc.get("/users?id=99")
.andExpect {
status { isOk }
}
verify(respository, times(1)).findAll()
}
}
话虽如此,这里有一些值得深思的地方:
Having said that, here's some food for thought:
-
任何测试都需要包括验证,以断言系统在各种类型的情况下均能按预期运行,包括诸如这样的负面情况.我们如何检查服务是否未能在服务中创建新的用户记录? DB .
我注意到您在ApplicationContext
( H2 )中已经有一个测试数据库设置,为什么不使用它来创建测试记录,而不仅仅是模拟存储库层?然后,您可以验证数据库是否包含任何新创建的记录.
I noticed you already have a Test DB setup in your ApplicationContext
(H2) so why not use it to create test records instead of just mocking the repository layer? Then you can verify the DB contains any newly created records.
作为一般规则,我避免将 Mockito 与Kotlin测试结合使用(出于某些原因,请搜索StackOverflow),甚至避免使用 mockito-kotlin .如今的最佳做法是将出色的 MockK 库与 AssertJ 或 assertk 用于验证您的期望.
As a general rule, I avoid using Mockito with Kotlin tests (search StackOverflow for a couple of reasons why), or even mockito-kotlin. Best practice nowadays is to use the excellent MockK library in combination with either AssertJ or assertk for verifying your expectations.
这篇关于Kotlin + SpringBootTest + Junit 5 + AutoConfigureMockMvc:应该在失败时通过测试(似乎@BeforeEach无效)的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!