spring-bootkotlinjunit5spring-test-mvcspringmockito

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


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.


Solution

  • 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: