Introduction to Testing in Kotlin: From Modules to User Experience
Kotlin is a modern language actively used for developing server-side (Ktor, Spring Boot), Android applications, and multiplatform projects. However, code quality is impossible without proper testing. In this article, we will cover three key levels: unit, integration, and e2e (end-to-end) testing. You will learn which tools to use, how to write tests in Kotlin, and how to build a reliable testing strategy for your application.
1. Unit Testing in Kotlin: The Foundation of Stability
Unit tests verify individual functions, classes, or methods in isolation from external dependencies. In Kotlin, JUnit 5 and MockK are most commonly used for this.
Tools and Setup
Add dependencies to build.gradle.kts:
dependencies { testImplementation("org.junit.jupiter:junit-jupiter:5.10.0") testImplementation("io.mockk:mockk:1.13.8")}Unit Test Example with MockK
Suppose we have a service that depends on a repository:
class UserService(private val repo: UserRepository) { fun getUserName(id: Int): String { val user = repo.findById(id) return user?.name ?: "Unknown" }}Test using MockK:
import io.mockk.everyimport io.mockk.mockkimport org.junit.jupiter.api.Assertions.assertEqualsimport org.junit.jupiter.api.Test
class UserServiceTest { @Test fun `getUserName returns name when user exists`() { val mockRepo = mockk<UserRepository>() every { mockRepo.findById(1) } returns User(1, "Alice") val service = UserService(mockRepo) assertEquals("Alice", service.getUserName(1)) }}Best Practices for Unit Tests
- One test — one assertion (use
assertwisely). - Isolate dependencies: use mocks (MockK) or stubs.
- Name tests clearly:
shouldReturnX_whenY. - Test edge cases: null, empty lists, exceptions.
2. Integration Testing: Verifying Component Interaction
Integration tests verify how real components work together: database, API, file system. In Kotlin, Testcontainers and Ktor Client (if you are writing a server) are convenient for this.
Example: Testing a Repository with PostgreSQL
Add Testcontainers and exposed (a library for working with databases):
dependencies { testImplementation("org.testcontainers:postgresql:1.19.3") implementation("org.jetbrains.exposed:exposed:0.44.1")}Test for UserRepository:
import org.junit.jupiter.api.*import org.testcontainers.containers.PostgreSQLContainerimport org.testcontainers.junit.jupiter.Containerimport org.testcontainers.junit.jupiter.Testcontainersimport org.jetbrains.exposed.sql.*
@Testcontainersclass UserRepositoryIntegrationTest {
@Container val postgres = PostgreSQLContainer<Nothing>("postgres:15").apply { withDatabaseName("testdb") withUsername("test") withPassword("test") }
private lateinit var repo: UserRepository
@BeforeEach fun setUp() { Database.connect(postgres.jdbcUrl, driver = "org.postgresql.Driver", user = postgres.username, password = postgres.password) transaction { SchemaUtils.create(Users) } repo = UserRepository() }
@Test fun `save and find user by id`() { val user = User(1, "Bob") repo.save(user) val found = repo.findById(1) assertEquals("Bob", found?.name) }
@AfterEa