- Framework:
kotlin.test(@Test,assertEquals,assertFailsWith,assertTrue, etc.) - Coroutines:
kotlinx-coroutines-test(runTestfor suspend functions) - Server:
ktor-server-test-host(testApplicationfor REST API tests) - Source sets: write tests in
commonTestby default; usejvmTest/iosTest/wasmJsTestonly for platform-specific behavior - No external assertion libraries — use
kotlin.testassertions only - No mocking libraries — write hand-crafted fakes (e.g.,
FakeHttpEngine)
Only write tests that verify meaningful behavior:
- Business logic — computed properties, state machines, algorithms, conditional branching
- Validation —
require/checkguards, input constraints, error paths - Edge cases — zero, one, negative, empty, boundary values, overflow
- Backward compatibility — deserializing old JSON formats without new fields
- Non-obvious behavior — semantics that could surprise a reader (e.g.,
Immediate != AfterDelay(0.seconds),distinctBykeeping first occurrence) - Custom serializers — types with hand-written serialization logic (e.g.,
SpeedLimit) - Integration points — verifying component wiring, delegation, fallback chains
Do not write tests for things the Kotlin language or frameworks already guarantee:
- Data class guarantees —
equals,hashCode,copy,toString, constructor storage - Enum guarantees —
entries,valueOf, declaration order, exhaustivewhen - Sealed class guarantees —
istype checks, subclass hierarchy - Value class guarantees — equality, identity
- Trivial default values — testing that
val x: Int = 0is indeed0 - Constructor parameter storage — testing that passing a value stores that value
- Basic kotlinx.serialization round-trips — simple encode/decode for
@Serializabletypes with no custom serializer (the framework guarantees this) - Test doubles — do not test fakes, mocks, or stubs themselves
- Prefer one test class per source class, in the same package under
commonTest - Use
runTestfor coroutine tests - Name tests descriptively:
functionName_condition_expectedResult - Keep tests focused — one assertion per logical concept
- Use
FakeHttpEngineand similar test doubles for isolation, but don't test the doubles - When a formula or algorithm lives in production code, test it by calling that production code — never reimplement the formula locally and assert against the reimplementation
Run the desktop app with ./gradlew :app:desktop:run.
The Ktor module has opt-in end-to-end tests using real HTTPS connections and temporary files:
./gradlew :library:ktor:jvmTest -PpublicDownloadTests=true --tests '*PublicDownloadTest'These verify a Git v2.46.0 README from GitHub against its SHA-256 checksum, plus HTTPBingo's 256 KiB deterministic range resource through a redirect and segmented pause/resume. They check file contents and completed segment progress. Each test has a 90-second timeout and cleans up its temporary files. Public service outages, rate limits, or network restrictions fail the tests; they are not silently treated as success.
Ordinary test runs exclude these tests. Opted-in runs bypass test-result caching and up-to-date
checks so they always exercise the network. Use :library:core:jvmTest for the offline regression
coverage of completed segment progress.
./gradlew :library:core:jvmTest :library:ktor:jvmTest :library:ftp:jvmTestHttpDownloadIntegrationTest uses a loopback HTTP server, real Ktor connections, temporary
output files, and SQLite task storage. It covers empty and small files, uneven segments,
servers without range support, invalid range responses, interrupted transfers, HTTP retries,
pause/resume, cancellation cleanup, live connection changes, changed server identity,
SQLite restart/resume, truncated local files, and UTF-8 server filenames. Each case has a
bounded timeout and closes its clients, server, database, and temporary files.
Common tests also check response validation without sockets and regression cases for segment progress snapshots, cancellation, and resegmentation. Keep fault-injection tests local so they remain reproducible without relying on a public server to misbehave.