Kotlin + SpringBootTest + Junit 5 + AutoConfigureMock Mvc: прохождение теста, когда должен был произойти сбой (кажется, @BeforeEach не вступает в силу) - PullRequest
0 голосов
/ 24 апреля 2020

Я кодировал очень простой и распространенный CRUD в Kotlin. Я хочу провести базовые c тесты в качестве тестового поста, удаления, получения и установки.

Возможно, я понял что-то не так: я использовал Beforeeach, чтобы вставить регистр, чтобы я мог проверить во время теста get. Я не получаю исключения, но кажется, что во время теста get он всегда возвращается нормально, когда он должен быть NOT_FOUND для любого другого идентификатора, отличного от 1 в тесте ниже.

Любая подсказка или указание в правильном направлении будет приветствоваться, даже если см. ниже дурную практику, основанную на моей цели (простой тест CRUD).

тест

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
)

зависимости gradle

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

В случае его полезности весь проект может быть загружен с https://github.com/jimisdrpc/games но я уверен, что все приведенные выше файлы достаточно для иллюстрации моей проблемы.

1 Ответ

1 голос
/ 25 апреля 2020

Чтобы решить вашу проблему, я предлагаю использовать @ MockBean , аннотацию, которую можно использовать для добавления макетов в Spring ApplicationContext.

. Я бы переписал ваш тест следующим образом (обратите внимание, что я пользуюсь тем, что mockito- kotlin уже является тестовой зависимостью вашего проекта):

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()
    }
}

Сказав это, вот некоторая пища для размышлений:

  • Любой тест должен включать проверку, чтобы утверждать, что системы ведут себя так, как ожидается при различных типах сценариев ios, включая негативный сценарий ios, например . Как мы можем проверить, службе не удалось создать новую запись пользователя в БД .

  • Я заметил, что в вашей ApplicationContext ( H2 ) уже есть настройка Test DB. ) так почему бы не использовать его для создания тестовых записей, а не просто издеваться над слоем хранилища? Затем вы можете проверить, что БД содержит все вновь созданные записи.

  • Как правило, я избегаю использования Mockito с Kotlin тестами (ищите StackOverflow для пары причин) или даже mockito- kotlin. В настоящее время рекомендуется использовать превосходную библиотеку MockK в сочетании с AssertJ или assertk для проверки ваших ожиданий.

...