Testing it, and the bugs the tests find
The program runs and the output looks right. But "looks right" is not "is right" — and for a program about money, you want proof. This final lesson writes the test suite that proves the settlement is correct, shows the bug that a test catches, and closes the course. Every test result here is from the real suite running under Gradle.
What to test, and why the design made it easy
Recall from the CLI lesson that the logic (netBalances, settle) is plain functions on plain data,
with no UI mixed in. That separation now pays off: you can test the logic directly, with no need to
simulate a command line or user input. This is not an accident — code is testable because it was
designed with the hard parts as pure functions. Untestable code is usually badly-structured code, and
the reverse holds too.
We test the things with rules and edge cases (the testing module's guidance): the balance arithmetic,
the settlement, and the validation. We do not test printReport — it is glue with no logic worth
proving.
The test suite
Here is the suite, in src/test/kotlin/splitter/SettlementTest.kt:
package splitter
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
import kotlin.test.assertFailsWith
class SettlementTest {
private val a = Person("A")
private val b = Person("B")
private val c = Person("C")
@Test
fun `an equal split leaves everyone even`() {
// A pays ₹300, shared 3 ways -> each owes ₹100, A is owed ₹200
val expenses = listOf(Expense("x", a, 30000, listOf(a, b, c)))
val balances = netBalances(expenses)
assertEquals(20000, balances[a])
assertEquals(-10000, balances[b])
assertEquals(-10000, balances[c])
}
@Test
fun `net balances always sum to zero`() {
val expenses = listOf(
Expense("hotel", a, 90000, listOf(a, b, c)),
Expense("food", b, 30000, listOf(a, b, c)),
)
assertEquals(0, netBalances(expenses).values.sum())
}
@Test
fun `settlement transfers match what is owed`() {
val expenses = listOf(Expense("x", a, 30000, listOf(a, b, c)))
val transfers = settle(expenses)
assertEquals(2, transfers.size)
assertTrue(transfers.all { it.to == a && it.amountPaise == 10000 })
}
@Test
fun `no expenses means no transfers`() {
assertTrue(settle(emptyList()).isEmpty())
}
@Test
fun `a negative amount is rejected`() {
assertFailsWith<IllegalArgumentException> {
Expense("bad", a, -100, listOf(a))
}
}
}
Read what each test proves, because together they are a specification of correct behaviour:
an equal split leaves everyone even— the core arithmetic. A pays ₹300 shared three ways, so A is owed ₹200 and B and C each owe ₹100. If this is wrong, the whole program is wrong.net balances always sum to zero— the invariant from the logic lesson, tested directly. If balances did not sum to zero, a settlement would be impossible; this test guards that property for every future change.settlement transfers match what is owed— the settlement produces exactly the right transfers: two, both to A, each ₹100.no expenses means no transfers— the empty edge case. Programs break on empty inputs; this proves ours does not.a negative amount is rejected— the failure path. TherequireinExpensemust fire on a negative amount, and this proves it does. Testing that validation works is as important as testing that valid input does.
Run them:
./gradlew test
BUILD SUCCESSFUL
5 tests, 0 failures, 100% successful
Five green tests, and now you know the settlement is correct — not because the output looked plausible, but because the arithmetic, the invariant, the transfers, the empty case, and the validation are all proven.
Watching a test catch a bug
Here is the payoff, and the testing module's most important habit made concrete. Suppose, refactoring
netBalances later, you fat-finger the credit line — a + becomes a -:
// the bug: crediting the payer with a MINUS
balances[e.paidBy] = (balances[e.paidBy] ?: 0) - e.amountPaise // should be +
The program still runs — no crash — and prints a report that is completely wrong (everyone owes
money, nobody is owed any). Without tests you might not notice until a friend complains the numbers
make no sense. With tests, the moment you run ./gradlew test:
SettlementTest > an equal split leaves everyone even FAILED
expected: <20000> but was: <-40000>
The test named the broken behaviour and pointed at the exact wrong number. You caught the bug in seconds, before it ever left your machine. That is what tests are for — not proving the code works once, but catching the day you break it. A codebase with tests like these is one you can change without fear; one without is one you are afraid to touch.
The course, complete
Step back and look at what you built: a complete Kotlin program in a proper Gradle project, with a clean model (data classes, integer money, self-validating), pure-function logic (collections and folds, no mutable tangle), a readable CLI (string templates, expressions, destructuring), and a test suite that proves it correct including the failure paths. Every piece traces to a lesson, and the whole thing is structured so the valuable parts — the model and the logic — are reusable and testable independent of the UI.
That is not a toy. It is the exact shape of the work you will be handed as an intern: model a domain, compute something over it, present it, and prove it works. You did it to the standard the best-practices, design-patterns, and testing modules set. You now know Kotlin — the language, its idioms, and how to write it well — which is the whole point of learning it before Android. When you start the Android course, the language will not be the new thing; only the framework will be. That is exactly where this course set out to get you, and you are there. Go and build something.
Check your work
Why the logic is easy to test. It is pure functions on plain data, with no UI mixed in — testable because it was well-structured.
What is worth testing here, and what is not. The arithmetic, the invariant, the settlement, and
the validation; not printReport, which is logicless glue.
What each test proves. The core arithmetic, the sum-to-zero invariant, correct transfers, the empty edge case, and that validation rejects bad input.
Why test the failure path. Validation is only proven correct when a test shows it fires — here, that a negative amount throws.
What a test suite is, beyond checking. A specification of correct behaviour, and a safety net that catches the day you break something.
What the +-to-- bug demonstrates. The program still runs but is completely wrong; the test
catches it in seconds with expected: <20000> but was: <-40000>.
The one habit tests embody. Verify by running — catch regressions immediately, change without fear.
What you have built, and what it means. A complete, tested, well-structured Kotlin program — the shape of real intern work — proving you know the language before you touch Android.
Practice
- Write all five tests and run
./gradlew test. Confirm 5 passing. - Introduce the
+-to--bug innetBalances, run the tests, and watch a test fail. Read the message. Fix it and confirm green. - Add a test for a trip where an expense is shared between only two of three people. Work out the expected balances by hand first.
- Add a test proving
sharePaisetruncates correctly for an amount that does not divide evenly. - Add a test for the case where everyone is already even (each pays their own share) —
settleshould return an empty list. - Break a different piece of logic (the
settletermination condition) and confirm a test catches it. - Extend the program with one new feature of your choice (unequal splits, or reading expenses from input), and write the tests for it first. Then make them pass.
- Clone the reference repository, run its tests, and compare its code with yours.
Official documentation
- Kotlin — Testing with kotlin.test — The assertions used here.
- Kotlin — Test code using JUnit — Running the suite with Gradle.
- Kotlin — Get started with Android — The next step: applying this Kotlin to Android.
- Android — Build your first app — Where the framework, and only the framework, becomes the new thing.
This is the end of the Kotlin Complete Foundation course. You can read and write idiomatic Kotlin, model a domain, transform data, write asynchronous code, test it, and structure a real project — the foundation the Android course assumes. Well done, and go and build something.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship