Introduction to Testing in Kotlin: from Unit to End-to-End
A high-quality codebase is impossible without automated testing. Kotlin, as a modern language for the JVM, provides developers with a rich toolkit for creating reliable tests. In this article, we will break down three levels of the testing pyramid as applied to Kotlin: unit, integration, and e2e. You will learn how to write effective tests in Kotlin using popular frameworks such as JUnit 5, MockK, Ktor Client, and Selenium.
1. Unit Testing in Kotlin: The Foundation of Stability
Unit tests verify the operation of individual functions, classes, or methods in isolation from external dependencies. In Kotlin, JUnit 5 is most often used for this, in combination with the mocking library MockK.
1.1. Project Setup
Add the dependencies to the build.gradle.kts file:
dependencies { testImplementation("org.junit.jupiter:junit-jupiter:5.10.0") testImplementation("io.mockk:mockk:1.13.8")}1.2. Unit Test Example with MockK
Suppose we have a UserService that depends on a repository:
class UserService(private val repository: UserRepository) { fun getUserName(id: Long): String { val user = repository.findById(id) return user?.name ?: "Unknown" }}Test using MockK:
import io.mockk.everyimport io.mockk.mockkimport org.junit.jupiter.api.Testimport kotlin.test.assertEquals
class UserServiceTest { @Test fun `return userName when user exists`() { val mockRepo = mockk<UserRepository>() every { mockRepo.findById(1L) } returns User(1L, "Alice") val service = UserService(mockRepo)
val result = service.getUserName(1L)
assertEquals("Alice", result) }}1.3. Best Practices for Unit Testing
- Isolate dependencies — use mocks for all external calls (database, API).
- Test edge cases: null, empty strings, negative numbers.
- Use clear test names — they should describe the scenario and the expected result.
- Follow the AAA principle: Arrange, Act, Assert.
2. Integration Testing in Kotlin: Checking Interaction
Integration tests verify how several system components work together. In Kotlin, this could be testing a REST API using Ktor Client or checking database operations via Exposed.
2.1. Testing REST API with Ktor
Create a simple Ktor server and test it:
// Serverfun Application.module() { routing { get("/hello") { call.respondText("Hello, Kotlin!") } }}
// Testclass ApiTest { @Test fun `test hello endpoint`() = testApplication { application { module() } val response = client.get("/hello") assertEquals(HttpStatusCode.OK, response.status) assertEquals("Hello, Kotlin!", response.bodyAsText()) }}2.2. Working with a Test Database
For integration tests with a database, use H2 (in-memory) or Testcontainers for PostgreSQL:
@Testfun `test user creation`() { withTestDatabase { val user = User.new { name = "Bob" email = "bob@test.com" } assertEquals("Bob", user.name) assertNotNull(user.id) }}
private fun withTestDatabase(block: () -> Unit) { Database.connect("jdbc:h2:mem: