Backend Unit Test with Kotlin and Spring Boot (JUnit&Mockk)

Boran Mert Koçan
Yıldız Tech
Published in
4 min readNov 22, 2023

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.

Resources

--

--