Testing in Kotlin: Unit, Integration, E2E Advanced Level

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Introduction: Why Advanced Testing is the Foundation of Reliable Kotlin Code

In modern Kotlin development, writing code is only half the battle. True mastery lies in the ability to create reliable, testable, and easily maintainable systems. Basic unit tests are familiar to everyone, but when a project grows to a microservice architecture or complex business logic, simple checks are no longer enough. Advanced testing includes not only isolating classes but also verifying interactions between components (integration tests) and fully reproducing user scenarios (E2E).

This article is intended for developers who are already familiar with the basics of Kotlin and want to take the quality of their projects to the next level. We will break down three key levels of testing: unit, integration, and e2e, using modern tools: MockK, Testcontainers, and Ktor Client. You will learn how to write tests that don't just "pass" but actually protect your code from regressions.



1. Unit Testing: Isolation and Mocks with MockK

Unit tests are the foundation of the testing pyramid. At an advanced level, we don't just check functions; we test complex interactions between objects using mocks. In Kotlin, the best library for this is MockK.



1.1. Advanced MockK Features

Unlike Mockito, MockK is written specifically for Kotlin and supports coroutines, lambdas, and extension functions. Let's look at an example of testing a service that uses a repository and coroutines:

// Service under testclass UserService(private val repo: UserRepository) {    suspend fun getUserName(id: Long): String {        val user = repo.findById(id)        return user?.name ?: "Unknown"    }}

// Testclass UserServiceTest { private val repo = mockk<UserRepository>() private val service = UserService(repo)

@Test fun `should return user name when user exists`() = runTest { // given coEvery { repo.findById(1L) } returns User(1, "Alice")

// when val result = service.getUserName(1L)

// then assertEquals("Alice", result) coVerify(exactly = 1) { repo.findById(1L) } }

@Test fun `should return Unknown when user not found`() = runTest { // given coEvery { repo.findById(any()) } returns null

// when val result = service.getUserName(99L)

// then assertEquals("Unknown", result) }}

Key points:

  • coEvery and coVerify — for working with suspend functions.
  • mockk() creates an empty mock, spyk() creates a partial mock of a real object.
  • Use slots to capture arguments.


1.2. Testing Lambdas and Extension Functions

MockK allows mocking extension functions, which is critical for frameworks like Ktor or Spring WebFlux:

class StringValidator {    fun String.isValidEmail(): Boolean = this.contains("@")}

// Test@Testfun `test extension mock`() { val validator = mockk<StringValidator>() with(validator) { every { "test@mail.com".isValidEmail() } returns true every { "invalid".isValidEmail() } returns false } assertTrue("test@mail.com".isValidEmail()) assertFalse("invalid".isValidEmail())}


2. Integration Testing: Database and Testcontainers

Integration tests check how your code interacts with real dependencies: databases, message queues, external APIs. Here, Testcontainers comes to the rescue — a library that spins up Docker containers directly during tests.



2.1. Setting up Testcontainers with PostgreSQL

For a Spring Boot application, it looks like this:

Blogs

Book Recommendations