Why TDD Is Not Just Another Trendy Methodology, But Your New Superhero Cape
Imagine: you write code like an artist — stroke by stroke, line by line. You're sure everything is brilliant. But an hour passes, you run the application, and it crashes with an error you'd spend half a day searching for. Sound familiar?
Now imagine a different scenario. You first write a test that is guaranteed to fail. Then — the minimal code to make the test pass. And only then — you refactor, improve, but with a clear conscience. This is Test-Driven Development (TDD), and in Kotlin it becomes a real pleasure.
In this article, we won't give boring lectures. We'll take a real example, go through all three stages of TDD (Red — Green — Refactor), and show you how to write reliable code without fear or reproach. Ready? Let's go!
TDD: Three Letters That Will Change Your Code
TDD (Test-Driven Development) is not just a technique, it's a philosophy. The essence is simple: test first, then code. Sounds crazy? At first glance — yes. In practice — it saves hours of debugging.
The Test-Driven Development Cycle
- Red — write a test that fails (it must fail).
- Green — write the minimal code to make the test pass.
- Refactor — improve the code without changing its behavior.
And so on — in a loop, in small steps. Each step is confidence that you haven't broken anything. In Kotlin, with its concise syntax and null safety, TDD becomes even more enjoyable.
Toolkit: What You Need for TDD in Kotlin
Before diving into the code, let's prepare the arsenal. For Kotlin unit testing you'll need:
- JUnit 5 — the de facto standard for unit tests.
- MockK — a mocking library built specifically for Kotlin (no more issues with null and companion objects).
- Gradle or Maven — for dependency management.
Add to build.gradle.kts:
dependencies { testImplementation("org.junit.jupiter:junit-jupiter:5.10.0") testImplementation("io.mockk:mockk:1.13.8")}Done. Now you can write code you won't be ashamed to show your colleagues.
Writing Your First TDD Cycle: An Ambitious Calculator
Let's start with a classic — a calculator. But not a simple one, one with string operation support. Task: the calculate function takes a string like "2 + 3" and returns 5.
Step 1: Red — Write a Failing Test
Create a test class:
import org.junit.jupiter.api.Testimport org.junit.jupiter.api.Assertions.assertEquals
class CalculatorTest {
@Test fun `should add two numbers`() { val calculator = StringCalculator() val result = calculator.calculate("2 + 3") assertEquals(5, result) }}Run it — the test fails because the StringCalculator class doesn't exist yet. Red signal — everything is going according to plan.
Step 2: Green — Minimal Code to Pass
Write just enough to make the test pass. No architectural frills:
class StringCalculator { fun calculate(expression: String): Int { val parts = expression.split(" + ") return parts[0].toInt() + parts[1].toInt() }}Now the test is green. Hooray! But the code is still ugly — it's only designed for adding two numbers.
Step 3: Refactor — Make the Code Beautiful
Refactor, but don't change the behavior. Add support for different operations, extract logic into separate functions:
class StringCalculator { fun calculate(expression: String): Int { val (a, op, b) =