RizTech Academy logo
RizTech Academy
Capstone: The Trip SplitterLesson 5 of 540 min

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. The require in Expense must 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

  1. Write all five tests and run ./gradlew test. Confirm 5 passing.
  2. Introduce the +-to-- bug in netBalances, run the tests, and watch a test fail. Read the message. Fix it and confirm green.
  3. 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.
  4. Add a test proving sharePaise truncates correctly for an amount that does not divide evenly.
  5. Add a test for the case where everyone is already even (each pays their own share) — settle should return an empty list.
  6. Break a different piece of logic (the settle termination condition) and confirm a test catches it.
  7. 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.
  8. Clone the reference repository, run its tests, and compare its code with yours.

Official documentation

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