Careeroza
Real World Example
Testing a Wallet Transfer Use Case
A UPI wallet must never debit money without crediting it, and must reject transfers above the balance. Business rules like these are tested with fast unit tests using fakes, on every pull request in CI. The tiny test helper below runs without JUnit so you can see the idea; in a real project the same checks are JUnit @Test functions with runTest.
Example
import kotlinx.coroutines.*
class InsufficientBalance(msg: String) : Exception(msg)
interface WalletStore {
suspend fun balance(user: String): Int
suspend fun set(user: String, amount: Int)
}
class FakeWalletStore(vararg start: Pair<String, Int>) : WalletStore {
val data = sortedMapOf(*start)
override suspend fun balance(user: String) = data[user] ?: 0
override suspend fun set(user: String, amount: Int) { data[user] = amount }
}
class TransferUseCase(private val store: WalletStore) {
suspend operator fun invoke(from: String, to: String, amount: Int) {
require(amount > 0) { "amount must be positive" }
val fromBal = store.balance(from)
if (fromBal < amount) throw InsufficientBalance("balance $fromBal < $amount")
store.set(from, fromBal - amount)
store.set(to, store.balance(to) + amount)
}
}
var passed = 0
fun test(name: String, body: suspend () -> Unit) = runBlocking {
try { body(); passed++; println("PASS $name") } catch (e: Throwable) { println("FAIL $name: ${e.message}") }
}
inline fun <reified E : Throwable> assertThrows(block: () -> Unit) {
try { block() } catch (e: Throwable) { if (e is E) return; throw AssertionError("wrong exception ${e::class.simpleName}") }
throw AssertionError("expected ${E::class.simpleName}")
}
fun main() {
test("moves money between wallets") {
val store = FakeWalletStore("asha" to 500, "ravi" to 100)
TransferUseCase(store)("asha", "ravi", 200)
check(store.data == sortedMapOf("asha" to 300, "ravi" to 300)) { "got ${store.data}" }
}
test("rejects transfer above balance and changes nothing") {
val store = FakeWalletStore("asha" to 50)
assertThrows<InsufficientBalance> { runBlocking { TransferUseCase(store)("asha", "ravi", 200) } }
check(store.data == sortedMapOf("asha" to 50)) { "got ${store.data}" }
}
test("rejects zero amount") {
assertThrows<IllegalArgumentException> { runBlocking { TransferUseCase(FakeWalletStore())("a", "b", 0) } }
}
println("$passed of 3 tests passed")
}Output
PASS moves money between wallets PASS rejects transfer above balance and changes nothing PASS rejects zero amount 3 of 3 tests passed
The JUnit Version
class TransferUseCaseTest {
@Test
fun `rejects transfer above balance and changes nothing`() = runTest {
val store = FakeWalletStore("asha" to 50)
assertThrows<InsufficientBalance> { TransferUseCase(store)("asha", "ravi", 200) }
assertEquals(mapOf("asha" to 50), store.data)
}
}Example
In CI (GitHub Actions) the tests run on every push:
./gradlew test --continueExample output
> Task :wallet:test TransferUseCaseTest > moves money between wallets() PASSED TransferUseCaseTest > rejects transfer above balance and changes nothing() PASSED TransferUseCaseTest > rejects zero amount() PASSED BUILD SUCCESSFUL in 6s