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.

Comments are closed on this article!