问题
I coded a very simple and common CRUD in Kotlin. I want to do basic tests as testing post, delete, get and put.
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.
Any clue or guidance in right direction will be wellcome even if see other bad practice bellow based on my purpose (simple CRUD test).
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)
}
}
controller
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())
// }
}
Repository
package com.mycomp.jokenpo.respository
import com.mycomp.jokenpo.model.User
import org.springframework.data.repository.CrudRepository
interface UserRepository : CrudRepository<User, Long>
Model
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
)
gradle dependencies
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
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
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.
回答1:
To solve your problem I suggest using @MockBean, an annotation that can be used to add mocks to a Spring ApplicationContext
.
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:
Any test needs to include verification to assert that the systems behaves as is expected under various types of scenarios including negative scenarios such as how do we check if the service failed to create a new User record in the DB.
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.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.
回答2:
To run get unit tests running in following setup:
- Kotlin
- Spring Boot
- JUnit 5
- Mockito
- Gradle
you need this configuration, to get started:
build.gradle.kts
dependencies {
// ...
testRuntimeOnly(group = "org.junit.jupiter", name = "junit-jupiter-engine", version = "5.6.3")
testImplementation(group = "org.mockito", name = "mockito-all", version = "1.10.19")
testImplementation("org.springframework.boot:spring-boot-starter-test") {
exclude(group = "org.junit.vin tage", module = "junit-vintage-engine")
}
// ...
}
tasks.withType<Test> {
useJUnitPlatform()
}
test file
@org.springframework.boot.test.context.SpringBootTest
class YourTest {
@org.mockito.Mock
lateinit var testingRepo: TestingRepo
@org.mockito.InjectMocks
lateinit var testingService: TestingService
@org.springframework.test.context.event.annotation.BeforeTestMethod
fun initMocks() {
org.mockito.MockitoAnnotations.initMocks(this)
}
@org.junit.jupiter.api.Test
fun yourTest() {org.junit.jupiter.api.Assertions.assertTrue(true)}
}
来源:https://stackoverflow.com/questions/61416636/kotlin-springboottest-junit-5-autoconfiguremockmvc-test-passing-when-it-w