
Getting Started with Unit Testing in Kotlin Spring Boot Applications
Learn how to write effective unit tests for your Kotlin Spring Boot application using JUnit 5 and Mockito, with practical examples and best practices
Unit testing is essential for maintaining reliable and maintainable Spring Boot applications. In this guide, we'll explore how to write effective unit tests using JUnit 5, Mockito, and AssertJ with Kotlin.
What is Unit Testing?
Unit testing is the practice of testing individual components or units of your code in isolation. In object-oriented programming, a "unit" is typically a single class or function. The goal is to verify that each unit works as intended, independent of other parts of the system.
Writing unit tests forces you to think about your code's design. If a piece of code is difficult to test, it often indicates potential design issues:
- Complex methods that do too many things
- Tight coupling between components
- Hidden dependencies
- Poor separation of concerns
This aligns with Clean Code principles, particularly:
- Single Responsibility Principle: Each class should have one reason to change
- Dependency Inversion: High-level modules shouldn't depend on low-level modules
- Interface Segregation: Clients shouldn't depend on methods they don't use
When you follow these principles, your code becomes naturally testable. Testable code is typically cleaner code, and clean code is inherently more testable.
Setting Up the Testing Environment
First, add the necessary dependencies to your build.gradle.kts
:
dependencies {
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
testImplementation("io.mockk:mockk:1.13.8")
testImplementation("com.ninja-squad:springmockk:4.0.2")
}
The spring-boot-starter-test
dependency includes:
- JUnit 5: The testing framework
- Mockito: For creating mock objects
- AssertJ: For fluent assertions
- Spring Test: For testing Spring components
MockK is included as an alternative to Mockito, designed specifically for Kotlin with support for coroutines and better null-safety.
Understanding the Basic Test Structure
Let's look at a basic test class structure in Kotlin:
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.BeforeEach
class CalculatorTest {
private lateinit var calculator: Calculator
@BeforeEach
fun setup() {
calculator = Calculator()
}
@Test
fun `should add two numbers correctly`() {
// given
val a = 2
val b = 3
// when
val result = calculator.add(a, b)
// then
assertEquals(5, result)
}
@Test
fun `should throw IllegalArgumentException when dividing by zero`() {
// given
val a = 10
val b = 0
// when/then
assertThrows<IllegalArgumentException> {
calculator.divide(a, b)
}
}
}
Key components of the test structure:
- Test Class: Named with the suffix "Test"
- Test Functions: Annotated with
@Test
- Setup Functions: Annotated with
@BeforeEach
for common initialization - Descriptive Names: Using backticks for readable test names
- Given-When-Then: Structure for organizing test logic
JUnit 5 Annotations
Common annotations you'll use:
@Test // Marks a method as a test case
@BeforeEach // Runs before each test method
@AfterEach // Runs after each test method
@BeforeAll // Runs once before all test methods (must be on companion object in Kotlin)
@AfterAll // Runs once after all test methods (must be on companion object in Kotlin)
@Disabled // Temporarily disables a test
@DisplayName("Custom test name") // Provides a custom name for test reporting
Example with multiple annotations:
class UserServiceTest {
companion object {
@JvmStatic
@BeforeAll
fun setupClass() {
// Run once before all tests
}
}
private lateinit var userService: UserService
@BeforeEach
fun setup() {
userService = UserService()
}
@Test
@DisplayName("Should create user with valid data")
fun createUser_withValidData_shouldSucceed() {
// Test implementation
}
@Test
@Disabled("Feature not implemented yet")
fun `should update user profile`() {
// Test implementation
}
}
Assertions with AssertJ
AssertJ provides a fluent API for assertions that's more readable than standard JUnit assertions:
import org.assertj.core.api.Assertions.*
class ProductTest {
@Test
fun `should validate product properties`() {
// given
val product = Product(
id = 1,
name = "Laptop",
price = BigDecimal("999.99"),
categories = listOf("Electronics", "Computers")
)
// then
assertThat(product.name)
.isNotBlank()
.startsWith("L")
.hasSize(6)
assertThat(product.price)
.isGreaterThan(BigDecimal.ZERO)
.isLessThan(BigDecimal("1000.00"))
assertThat(product.categories)
.hasSize(2)
.contains("Electronics")
.doesNotContain("Food")
}
}
AssertJ makes tests more readable and provides helpful error messages when assertions fail.
Testing Best Practices
- Follow the AAA Pattern:
- Arrange: Set up the test data
- Act: Execute the method being tested
- Assert: Verify the results
- One Assert Per Test:
- Focus each test on a single behavior
- Makes it easier to identify what failed
- Exception: When testing related properties of an object
- Meaningful Names:
- Use descriptive test names that explain the scenario
- Include expected behavior in the name
- Use underscores or backticks for readability
- Keep Tests Independent:
- Each test should run independently
- Don't rely on test execution order
- Reset state in
@BeforeEach
when needed