Backend Unit Test with Kotlin and Spring Boot (JUnit&Mockk)
Unit tests are used to test the smallest units of code. These tests verify the accuracy and functionality of the smallest units, ensuring that they work as expected. Their purpose is to identify and rectify errors early by testing each individual part of the code separately. Unit tests contribute to improving code quality and ensuring its reliability.
In this article, I will discuss how to write unit tests in a backend service written in Kotlin and Spring Boot using the JUnit and Mockk libraries.
Let’s say we have a UserService
class that contains a method that takes the user’s first name and last name as parameters, concatenates them, and returns the result:
@Service
class UserService {
fun combineNames(firstName: String, lastName: String): String {
return "$firstName $lastName"
}
}
Now, let’s write a unit test for the combineNames
method in the UserService
class:
class UserServiceTest {
private val userService = UserService()
@Test
fun testCombineNames() {
// Given
val firstName = "John"
val lastName = "Doe"
val expected = "John Doe"
// When
val actual = userService.combineNames(firstName, lastName)
// Then
assertEquals(expected, actual)
}
}
The unit test above is testing the combineNames
method of the UserService
class. Here, we are invoking the combineNames
method from the UserService
class with the parameters we have defined. Then, we compare the result obtained from calling this method with our expected result using the assertEquals
method. If the result returned from the method call matches our expected result, the test will pass successfully; otherwise, it will fail.
Now, let’s make a small addition to the combineNames
method. This method will also be called the integrateUser
method of the UserIntegrationService
class:
@Service
class UserService(private val userIntegrationService: UserIntegrationService) {
fun combineNames(firstName: String, lastName: String): String {
val fullName = "$firstName $lastName"
userIntegrationService.integrateUser(firstName, lastName)
return fullName
}
}
In such a scenario, we can make our unit test more detailed as follows:
class UserServiceTest {
private val userIntegrationService = mockk<UserIntegrationService>()
private val userService = UserService(userIntegrationService)
@Test
fun testCombineNames() {
// Given
val firstName = "John"
val lastName = "Doe"
val expected = "John Doe"
every { userIntegrationService.integrateUser(firstName, lastName) } returns Unit
// When
val actual = userService.combineNames(firstName, lastName)
// Then
assertEquals(expected, actual)
verify(exactly = 1) { userIntegrationService.integrateUser(firstName, lastName) }
}
}
Unlike the first unit test we wrote, in this test, we are verifying whether the integrateUser
method is being called within the combineNames
method using the verify
statement. By specifying exactly = 1
inside verify
, we ensure that for this test to pass, the integrateUser
method of the UserIntegrationService
class must be called exactly once. Otherwise, the test will fail.
Another line of code that we added here is the mocking of the UserIntegrationService
class that we call from the UserService
class. The purpose here is to isolate the unit we are testing from its other dependencies.
Now, let’s make one more addition to the combineNames
method. For example, if the firstName
or lastName
parameters are empty, let's throw an error:
@Service
class UserService(private val userIntegrationService: UserIntegrationService) {
fun combineNames(firstName: String, lastName: String): String {
if (firstName.isBlank() || lastName.isBlank()) {
throw IllegalArgumentException("Kullanıcının adı ve soyadı boş olamaz.")
}
val fullName = "$firstName $lastName"
userIntegrationService.integrateUser(firstName, lastName)
return fullName
}
}
In this case, to write a comprehensive unit test, we need to test the scenario where the error is thrown:
class UserServiceTest {
private val userIntegrationService = mockk<UserIntegrationService>()
private val userService = UserService(userIntegrationService)
@Test
fun testCombineNames() {
// Given
val firstName = "John"
val lastName = "Doe"
val expected = "John Doe"
every { userIntegrationService.integrateUser(firstName, lastName) } returns Unit
// When
val actual = userService.combineNames(firstName, lastName)
// Then
assertEquals(expected, actual)
verify(exactly = 1) { userIntegrationService.integrateUser(firstName, lastName) }
}
@Test
fun testCombineNames_WithEmptyName() {
// Given
val firstName = ""
val lastName = "Doe"
// When - Then
assertThrows(IllegalArgumentException::class.java) {
userService.combineNames(firstName, lastName)
}
}
}
In the new unit test added above, it is expected that an error will be thrown when the firstName
field is empty. If the conditions are met, the test will succeed. If an error is not thrown in the opposite case, the test will fail.
In this article, I aimed to demonstrate how unit tests can be written in a backend service written in Kotlin and Spring Boot using the JUnit and Mockk libraries with simple examples. These examples can be expanded upon. With the capabilities provided by JUnit and Mockk libraries, it is possible to write unit tests for different conditions.
Unit tests are an important development process that enhances the reliability of the code and helps identify errors at an early stage.
We should write comprehensive and accurate unit tests.