Kotlin + SpringBootTest + Junit 5 + AutoConfigureMockMvc:应该在失败时通过测试(似乎@BeforeEach无效) [英] Kotlin + SpringBootTest + Junit 5 + AutoConfigureMockMvc: test passing when it was supposed to fail (seems @BeforeEach not taking effect)

查看:250
本文介绍了Kotlin + SpringBootTest + Junit 5 + AutoConfigureMockMvc:应该在失败时通过测试(似乎@BeforeEach无效)的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我在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.

推荐答案

为解决您的问题,我建议使用

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),甚至避免使用 MockK 库与 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无效)的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆