Testing in Kotlin: unit, integration, e2e from A to Z
\n\nModern development in Kotlin requires a systematic approach to testing. Whether you are writing a backend on Ktor or a mobile application for Android, high-quality test coverage is the key to project stability and predictability. In this article, we will cover three levels of testing: unit, integration, and e2e tests. You will learn which tools to use, how to write effective tests in Kotlin, and how to avoid common mistakes.
\n\n1. Unit testing in Kotlin: the foundation
\n\nUnit tests verify the work of individual functions, classes, or methods in isolation from external dependencies. In Kotlin, the JUnit 5 framework is most often used for this, combined with the mocking library MockK.
\n\nProject setup
\n\nAdd dependencies to build.gradle.kts:
dependencies {\n testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")\n testImplementation("io.mockk:mockk:1.13.8")\n}\n\n\nUnit test example
\n\nSuppose we have a simple service for calculating a discount:
\n\nclass DiscountService {\n fun applyDiscount(price: Double, discountPercent: Int): Double {\n require(discountPercent in 0..100) { "Discount percentage must be between 0 and 100" }\n return price * (100 - discountPercent) / 100\n }\n}\n\n\nTest for this service:
\n\nimport org.junit.jupiter.api.Assertions.*\nimport org.junit.jupiter.api.Test\nimport org.junit.jupiter.api.assertThrows\n\nclass DiscountServiceTest {\n\n private val service = DiscountService()\n\n @Test\n fun `applyDiscount correctly calculates a 50 percent discount`() {\n val result = service.applyDiscount(1000.0, 50)\n assertEquals(500.0, result, 0.001)\n }\n\n @Test\n fun `applyDiscount throws an exception for an invalid percentage`() {\n assertThrows<IllegalArgumentException> {\n service.applyDiscount(100.0, 150)\n }\n }\n}\n\n\nTip: Use assertThrows to check exceptions and name tests with clear strings — this improves report readability.
Mocking with MockK
\n\nWhen your class depends on other components (e.g., a repository or API), use mocks:
\n\nclass OrderService(private val paymentGateway: PaymentGateway) {\n fun processOrder(amount: Double): String {\n if (paymentGateway.charge(amount)) {\n return "Payment successful"\n }\n return "Payment error"\n }\n}\n\n// Test\n@Test\nfun `processOrder returns success on successful payment`() {\n val mockGateway = mockk<PaymentGateway>()\n every { mockGateway.charge(any()) } returns true\n\n val service = OrderService(mockGateway)\n val result = service.processOrder(500.0)\n\n assertEquals("Payment successful", result)\n verify { mockGateway.charge(500.0) }\n}\n\n\n2. Integration testing: verifying component interaction
\n\nIntegration tests check how different parts of the system work together: database, external APIs, file system. Testcontainers is an excellent choice for Kotlin — a library for spinning up Docker containers during tests.
\n\nExample with PostgreSQL database
\n\nAdd dependencies:
\n\ntestImplementation("org.testcontainers:testcontainers:1.19.3")\ntestImplementation("org.testcontainers:postgresql:1.19.3")\n\n\nLet's write a test for the DAO layer:
\n\nclass UserRepositoryIntegrationTest {\n\n companion object {\n private val postgres = PostgreSQLContainer<Nothing>("postgres:15").apply {\n withDatabaseName("testdb")\n withUsername("test")\n withPassword("test")\n start()\n