Kotlin + SpringBootTest + Junit 5 + AutoConfigureMockMvc: test passing when it was supposed to fail (seems @BeforeEach not taking effect)

大城市里の小女人 提交于 2021-02-11 04:52:41

问题


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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!