Spring Boot Testing with Kotlin: @SpringBootTest, MockMvc, and TestRestTemplate

Spring Boot Testing with Kotlin: @SpringBootTest, MockMvc, and TestRestTemplate

Spring Boot's test support is comprehensive, and Kotlin's concise syntax makes test setup significantly less verbose than the equivalent Java. This guide covers the main testing strategies for Spring Boot applications written in Kotlin.

Dependencies

// build.gradle.kts
dependencies {
    testImplementation("org.springframework.boot:spring-boot-starter-test")
    testImplementation("io.mockk:mockk:1.13.12")
    testImplementation("com.ninja-squad:springmockk:4.0.2")  // MockK + Spring integration
}

spring-boot-starter-test includes JUnit 5, Mockito, AssertJ, and Spring Test. springmockk replaces Mockito's @MockBean with @MockkBean for Kotlin-native mocking.

Full Integration Tests: @SpringBootTest

@SpringBootTest boots the full application context:

import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.web.client.TestRestTemplate
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.http.HttpStatus
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Assertions.*

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class UserControllerIntegrationTest {

    @Autowired
    lateinit var restTemplate: TestRestTemplate

    @Test
    fun `GET users returns 200`() {
        val response = restTemplate.getForEntity("/api/users", String::class.java)
        assertEquals(HttpStatus.OK, response.statusCode)
    }

    @Test
    fun `POST user creates and returns new user`() {
        val request = CreateUserRequest("Alice", "alice@example.com")
        val response = restTemplate.postForEntity(
            "/api/users",
            request,
            UserResponse::class.java
        )
        assertEquals(HttpStatus.CREATED, response.statusCode)
        assertEquals("Alice", response.body?.name)
    }
}

RANDOM_PORT starts a real HTTP server on a random port — integration tests hit a running server.

TestRestTemplate with Auth

@Test
fun `protected endpoint requires authentication`() {
    val response = restTemplate
        .withBasicAuth("user", "password")
        .getForEntity("/api/admin", String::class.java)
    assertEquals(HttpStatus.OK, response.statusCode)
}

Web Layer Tests: @WebMvcTest

@WebMvcTest loads only the web layer (controllers, filters, security) without starting a server — faster than @SpringBootTest:

import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.*
import com.ninjasquad.springmockk.MockkBean
import io.mockk.every

@WebMvcTest(UserController::class)
class UserControllerTest {

    @Autowired
    lateinit var mockMvc: MockMvc

    @MockkBean
    lateinit var userService: UserService

    @Test
    fun `GET user by id returns user JSON`() {
        every { userService.findById(1L) } returns User(1L, "Alice", "alice@example.com")

        mockMvc.perform(get("/api/users/1"))
            .andExpect(status().isOk)
            .andExpect(jsonPath("$.name").value("Alice"))
            .andExpect(jsonPath("$.email").value("alice@example.com"))
    }

    @Test
    fun `GET user with invalid id returns 404`() {
        every { userService.findById(999L) } throws UserNotFoundException(999L)

        mockMvc.perform(get("/api/users/999"))
            .andExpect(status().isNotFound)
    }

    @Test
    fun `POST user with missing name returns 400`() {
        mockMvc.perform(
            post("/api/users")
                .contentType("application/json")
                .content("""{"email": "alice@example.com"}""")
        )
            .andExpect(status().isBadRequest)
    }
}

@MockkBean replaces @MockBean from Mockito, using MockK under the hood.

Kotlin MockMvc DSL

Spring provides a Kotlin-idiomatic MockMvc DSL:

import org.springframework.test.web.servlet.get
import org.springframework.test.web.servlet.post

@Test
fun `get user returns user`() {
    mockMvc.get("/api/users/1").andExpect {
        status { isOk() }
        jsonPath("$.name") { value("Alice") }
        content { contentType(MediaType.APPLICATION_JSON) }
    }
}

@Test
fun `create user`() {
    mockMvc.post("/api/users") {
        contentType = MediaType.APPLICATION_JSON
        content = """{"name": "Alice", "email": "alice@example.com"}"""
    }.andExpect {
        status { isCreated() }
        jsonPath("$.id") { isNumber() }
    }
}

The Kotlin DSL is more concise than the Java-style MockMvcRequestBuilders and MockMvcResultMatchers.

Repository Tests: @DataJpaTest

@DataJpaTest loads only the JPA layer with an in-memory H2 database:

import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager

@DataJpaTest
class UserRepositoryTest {

    @Autowired
    lateinit var entityManager: TestEntityManager

    @Autowired
    lateinit var userRepository: UserRepository

    @Test
    fun `find by email returns user`() {
        val user = User(name = "Alice", email = "alice@example.com")
        entityManager.persist(user)
        entityManager.flush()

        val found = userRepository.findByEmail("alice@example.com")

        assertNotNull(found)
        assertEquals("Alice", found?.name)
    }

    @Test
    fun `find by email returns null when not found`() {
        val result = userRepository.findByEmail("missing@example.com")
        assertNull(result)
    }
}

TestEntityManager gives direct JPA access for seeding test data without going through the repository layer.

Service Layer Tests with Mocks

Service tests don't need Spring context at all:

import io.mockk.mockk
import io.mockk.every
import io.mockk.verify
import org.junit.jupiter.api.Test

class UserServiceTest {

    private val repository = mockk<UserRepository>()
    private val emailService = mockk<EmailService>(relaxed = true)
    private val service = UserService(repository, emailService)

    @Test
    fun `createUser saves user and sends welcome email`() {
        val savedUser = User(id = 1L, name = "Alice", email = "alice@example.com")
        every { repository.save(any()) } returns savedUser

        val result = service.createUser("Alice", "alice@example.com")

        assertEquals("Alice", result.name)
        verify { repository.save(match { it.name == "Alice" }) }
        verify { emailService.sendWelcome("alice@example.com") }
    }
}

Pure unit tests run in milliseconds because they load no application context.

Configuration for Tests

Test Application Properties

src/test/resources/application-test.properties:

spring.datasource.url=jdbc:h2:mem:testdb
spring.jpa.hibernate.ddl-auto=create-drop
spring.mail.host=localhost

Activate in tests:

@SpringBootTest
@ActiveProfiles("test")
class IntegrationTest { ... }

@TestConfiguration

Override beans for tests:

@TestConfiguration
class TestConfig {
    @Bean
    @Primary
    fun mockEmailService(): EmailService = mockk(relaxed = true)
}

Testing Security

import org.springframework.security.test.context.support.WithMockUser
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*

@WebMvcTest(AdminController::class)
class AdminControllerTest {

    @Test
    @WithMockUser(roles = ["ADMIN"])
    fun `admin can access dashboard`() {
        mockMvc.get("/admin/dashboard").andExpect {
            status { isOk() }
        }
    }

    @Test
    fun `unauthenticated user is redirected`() {
        mockMvc.get("/admin/dashboard").andExpect {
            status { is3xxRedirection() }
        }
    }

    @Test
    fun `csrf protection on POST`() {
        mockMvc.post("/admin/users") {
            contentType = MediaType.APPLICATION_JSON
            content = """{"name": "Alice"}"""
        }.andExpect {
            status { isForbidden() }  // no CSRF token
        }
    }
}

Test Strategy by Layer

Layer Annotation What Loads Speed
Unit None Nothing Fastest
Repository @DataJpaTest JPA + H2 Fast
Web @WebMvcTest Controllers Fast
Integration @SpringBootTest Everything Slow

Run unit and slice tests in every PR. Full integration tests in CI on merge to main.

Production Monitoring

Spring Boot tests validate application behavior against local or in-memory dependencies. For production monitoring — ensuring your deployed Spring Boot service handles real traffic correctly — HelpMeTest runs behavioral tests against live endpoints 24/7.

Summary

  • @SpringBootTest(webEnvironment = RANDOM_PORT) + TestRestTemplate for full integration tests
  • @WebMvcTest + MockMvc for web layer tests without starting a server
  • Kotlin MockMvc DSL (mockMvc.get { }) is more concise than Java-style builders
  • @DataJpaTest + TestEntityManager for repository tests with in-memory database
  • @MockkBean from springmockk replaces @MockBean for Kotlin-native mocking
  • Service tests don't need Spring context — use plain MockK for speed

Read more