1. Overview
In this tutorial, we’ll explore a few testing techniques for a Kotlin-based Spring Boot application.
First, let’s create a basic Kotlin-based Spring Boot app with a few components like the repository, controller, and service. Then, we can discuss the unit and integration testing techniques.
2. Setup
Let’s brush up on the Maven dependencies for the Spring Boot app with Kotlin.
First, we’ll add the latest spring-boot-starter-web and spring-boot-starter-data-jpa Maven dependencies to our pom.xml for web and JPA support:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.1.4</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>3.1.4</version>
</dependency>
Then, let’s add the h2 embedded database for persistence:
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
<version>2.0.202</version>
</dependency>
Next, we can define the source directories of the Kotlin code:
<build>
<sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
<testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
<plugins>
// ...
</plugins>
</build>
Last, we’ll set up the kotlin-maven-plugin plugin to provide compiler plugins like all-open and no-arg for Spring and JPA support:
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<configuration>
<args>
<arg>-Xjsr305=strict</arg>
</args>
<compilerPlugins>
<plugin>spring</plugin>
<plugin>jpa</plugin>
<plugin>all-open</plugin>
<plugin>no-arg</plugin>
</compilerPlugins>
<pluginOptions>
<option>all-open:annotation=javax.persistence.Entity</option>
<option>all-open:annotation=javax.persistence.Embeddable</option>
<option>all-open:annotation=javax.persistence.MappedSuperclass</option>
</pluginOptions>
</configuration>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-allopen</artifactId>
<version>1.8.0</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-noarg</artifactId>
<version>1.8.0</version>
</dependency>
</dependencies>
</plugin>
3. Spring Boot Application
Now that our setup is ready. Let’s add a few components to our Spring Boot app for unit and integration testing.
3.1. Entity
First, we’ll create the BankAccount entity with a few properties like bankCode and accountNumber:
@Entity
data class BankAccount (
var bankCode:String,
var accountNumber:String,
var accountHolderName:String,
@Id @GeneratedValue var id: Long? = null
)
3.2. Repository
Then, let’s create the BankAccountRepository class to provide CRUD features on the BankAccount entity using Spring Data’s CrudRepository:
@Repository
interface BankAccountRepository : CrudRepository<BankAccount, Long> {}
3.3. Service
Further, we’ll create the BankAccountService class with a few methods like addBankAccount and getBankAccount:
@Service
class BankAccountService(var bankAccountRepository: BankAccountRepository) {
fun addBankAccount(bankAccount: BankAccount): BankAccount {
return bankAccountRepository.save(bankAccount);
}
fun getBankAccount(id: Long): BankAccount? {
return bankAccountRepository.findByIdOrNull(id)
}
}
3.4. Controller
Last, let’s create the BankController class to expose the /api/bankAccount endpoint:
@RestController
@RequestMapping("/api/bankAccount")
class BankController(var bankAccountService: BankAccountService) {
@PostMapping
fun addBankAccount(@RequestBody bankAccount:BankAccount) : ResponseEntity<BankAccount> {
return ResponseEntity.ok(bankAccountService.addBankAccount(bankAccount))
}
@GetMapping
fun getBankAccount(@RequestParam id:Long) : ResponseEntity<BankAccount> {
var bankAccount: BankAccount? = bankAccountService.getBankAccount(id);
if (bankAccount != null) {
return ResponseEntity(bankAccount, HttpStatus.OK)
} else {
return ResponseEntity(HttpStatus.BAD_REQUEST)
}
}
}
So, we’ve exposed the endpoint with the GET and POST mapping to read and create the BankAccount object, respectively.
4. Testing Setup
4.1. JUnit5
First, we’ll exclude the JUnit’s vintage support, which is part of the spring-boot-starter-test dependency:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
Then, let’s include the Maven dependencies for JUnit5 support:
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.8.1</version>
<scope>test</scope>
</dependency>
4.2. MockK
Also, we should add mocking capabilities to our tests that prove handy in testing service and repository components.
However, instead of Mockito, we’ll use the MockK library, which is better suited for Kotlin.
So, let’s exclude the mockito-core dependency that comes with the spring-boot-starter-test dependency:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
</exclusion>
</exclusions>
</dependency>
Then, we can add the latest mockk Maven dependency to our pom.xml:
<dependency>
<groupId>com.ninja-squad</groupId>
<artifactId>springmockk</artifactId>
<version>3.0.1</version>
<scope>test</scope>
</dependency>
5. Unit Tests
5.1. Test Service Using MockK
First, let’s create the BankAccountServiceTest class and mock the BankAccountRepository using MockK:
class BankAccountServiceTest {
val bankAccountRepository: BankAccountRepository = mockk();
val bankAccountService = BankAccountService(bankAccountRepository);
}
Then, we can use every block to mock the response of the BankAccountRepository and verify the result of the getBankAccount method:
@Test
fun whenGetBankAccount_thenReturnBankAccount() {
//given
every { bankAccountRepository.findByIdOrNull(1) } returns bankAccount;
//when
val result = bankAccountService.getBankAccount(1);
//then
verify(exactly = 1) { bankAccountRepository.findByIdOrNull(1) };
assertEquals(bankAccount, result)
}
5.2. Test Controller Using @WebMvcTest
We can use the @WebMvcTest annotation that automatically configures the Spring MVC infrastructure for our unit tests.
First, we’ll inject the MockMvc bean and mock the BankAccountService using the @MockkBean annotation:
@WebMvcTest
class BankControllerTest(@Autowired val mockMvc: MockMvc) {
@MockkBean
lateinit var bankAccountService: BankAccountService
}
Then, we can mock the response of BankAccountService, use an instance of the MockMvc bean to perform a GET request, and verify the JSON result:
@Test
fun givenExistingBankAccount_whenGetRequest_thenReturnsBankAccountJsonWithStatus200() {
every { bankAccountService.getBankAccount(1) } returns bankAccount;
mockMvc.perform(get("/api/bankAccount?id=1"))
.andExpect(status().isOk)
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.bankCode").value("ING"));
}
Similarly, we can verify a GET request that results in a bad request:
@Test
fun givenBankAccountDoesntExist_whenGetRequest_thenReturnsStatus400() {
every { bankAccountService.getBankAccount(2) } returns null;
mockMvc.perform(get("/api/bankAccount?id=2"))
.andExpect(status().isBadRequest());
}
Also, we can use the MockMvc bean to perform a POST request with the BankAccount JSON as the request body and verify the result:
@Test
fun whenPostRequestWithBankAccountJson_thenReturnsStatus200() {
every { bankAccountService.addBankAccount(bankAccount) } returns bankAccount;
mockMvc.perform(post("/api/bankAccount").content(mapper.writeValueAsString(bankAccount)).contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk)
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.bankCode").value("ING"));
}
6. Integration Tests
6.1. Test Repository Using @DataJpaTest
We can use the @DataJpaTest annotation that provides a standard setup for the persistence layer to test a repository.
First, we’ll create the BankAccountRepositoryTest class and inject the TestEntityManager bean that rollbacks the entire execution once the test is over:
@DataJpaTest
class BankAccountRepositoryTest {
@Autowired
lateinit var entityManager: TestEntityManager
@Autowired
lateinit var bankAccountRepository: BankAccountRepository
}
Then, let’s test the findByIdOrNull extension method of the BankAccountRepository using an instance of the TestEntityManager:
@Test
fun WhenFindById_thenReturnBankAccount() {
val ingBankAccount = BankAccount("ING", "123ING456", "JOHN SMITH");
entityManager.persist(ingBankAccount)
entityManager.flush()
val ingBankAccountFound = bankAccountRepository.findByIdOrNull(ingBankAccount.id!!)
assertThat(ingBankAccountFound == ingBankAccount)
}
6.2. Test App Using @SpringBootTest
We can use the @SpringBootTest annotation to start our app in a sandbox web environment:
@SpringBootTest(
classes = arrayOf(KotlinTestingDemoApplication::class),
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class KotlinTestingDemoApplicationIntegrationTest {
@Autowired
lateinit var restTemplate: TestRestTemplate
}
Also, we’ve injected the TestRestTemplate bean to test the RESTful endpoints exposed by our app.
So, let’s test the GET request on the /api/bankAccount endpoint:
@Test
fun whenGetCalled_thenShouldBadReqeust() {
val result = restTemplate.getForEntity("/api/bankAccount?id=2", BankAccount::class.java);
assertNotNull(result)
assertEquals(HttpStatus.BAD_REQUEST, result?.statusCode)
}
Similarly, we can use the TestRestTemplate instance to test the POST request on the /api/bankAccount endpoint:
@Test
fun whePostCalled_thenShouldReturnBankObject() {
val result = restTemplate.postForEntity("/api/bankAccount", BankAccount("ING", "123ING456", "JOHN SMITH"), BankAccount::class.java);
assertNotNull(result)
assertEquals(HttpStatus.OK, result?.statusCode)
assertEquals("ING", result.getBody()?.bankCode)
}
7. Conclusion
In this article, we’ve discussed a few unit and integration testing techniques for the Spring Boot app with Kotlin.
First, we developed a Kotlin-based Spring Boot app with an entity, repository, service, and controller. Then, we explored different ways to test such components.
As always, the code is available on GitHub.