name: kmp-testing-strategy description: This skill should be used when planning test approach, writing tests, and analyzing coverage. Use for Kotest, MockK, property tests, and coverage analysis.
When to Use
Triggers:
- "write tests for", "add test coverage", "create test file"
- "plan testing approach", "testing strategy", "test design"
- "analyze coverage", "check test coverage", "coverage report"
- "mock repository", "mock ViewModel", "test with MockK"
- "property test", "property-based testing", "checkAll", "forAll"
- "flow test", "StateFlow test", "SharedFlow test", "test with Turbine"
- "run tests", "execute tests", "test execution"
Exclusions:
- Do NOT use for iOS-specific testing setup (use @kmp-ios skill for iOS)
- Do NOT use for UI screenshot testing setup (use Roborazzi-specific guides)
- Do NOT use for build configuration issues (use convention plugins guide)
Mode Detection
| User Request | Reference File | Load When |
|---|---|---|
| "Property test examples" / "write property-based tests" | property-testing-examples.md | MANDATORY |
MANDATORY - READ ENTIRE FILE: Before writing property-based tests, you MUST read property-testing-examples.md (~130 lines) for Kotest property test patterns, custom Arb generators, and coverage targets specific to this project.
Do NOT load property-testing-examples.md when:
- Writing only concrete tests (repository success paths, ViewModel state transitions)
- Running or analyzing existing tests
- Setting up test infrastructure
Decision Framework
Before planning tests, ask yourself:
What coverage level is required?
- Mappers (DTO→Domain) → 100% property-based test coverage (MANDATORY)
- ViewModels → All state transitions + error paths
- Repositories → Success path + all error types (Network, Http, Unknown)
- Overall target → 40% property tests, 60% concrete tests
What testing tools should I use?
- State flows → Turbine (NEVER use Thread.sleep or delays)
- Mocking → MockK for interfaces, Fake implementations for complex logic
- Property tests → Kotest with Arb generators
- Coroutines → TestScope + StandardTestDispatcher
How do I verify test quality?
- Run full suite:
./gradlew test --continue(114 tests passing baseline) - Check coverage: All production code has corresponding test file
- Verify patterns: Property tests for mappers, Turbine for flows, all error paths
- NEVER skip tests when adding production code
- Run full suite:
Essential Workflows
Workflow 1: Write Repository Tests with Kotest + MockK
- Create test file in
androidUnitTest/source set with*Test.ktor*Spec.ktsuffix - Use Kotest spec (
StringSpec,FreeSpec,DescribeSpec) for test structure - Mock dependencies with MockK:
val mockApi = mockk<ApiService>(relaxed = true) - Stub methods with
coEvery { mockApi.method() } returns value - Test Either returns with Kotest Arrow extensions:
result.shouldBeRight()orresult.shouldBeLeft() - Cover all error paths: Network, Http (with codes 400-599), Unknown
- Verify interactions with
coVerify { mockApi.method() } - Clean up mocks in
afterTestif needed
Example:
class PokemonListRepositoryTest : StringSpec({
lateinit var mockApi: PokemonListApiService
lateinit var repository: PokemonListRepository
beforeTest {
mockApi = mockk(relaxed = true)
repository = PokemonListRepository(mockApi)
}
"returns Right on success" {
coEvery { mockApi.getPokemonList(20, 0) } returns mockDto
val result = repository.loadPage(20, 0)
result.shouldBeRight().pokemons shouldNotBeEmpty()
}
"returns Left on network error" {
coEvery { mockApi.getPokemonList(any(), any()) } throws IOException()
val result = repository.loadPage(20, 0)
result.shouldBeLeft() shouldBe RepoError.Network
}
})
Workflow 2: Write ViewModel Tests with Turbine + TestScope
- Create test file in
androidUnitTest/source set - Create
TestScopeandTestDispatcher:val testScope = TestScope(StandardTestDispatcher()) - Inject
testScopeinto ViewModel constructor (notDispatchers.Main) - Mock repository with MockK:
val mockRepository = mockk<Repository>(relaxed = true) - Use Turbine
.test { }to collect StateFlow emissions - Call ViewModel methods directly (not through lifecycle)
- Advance time with
testScope.advanceUntilIdle() - Verify state transitions:
awaitItem()for initial state, after action, after advance - Clean up with
cancelAndIgnoreRemainingEvents()at end - NO
Dispatchers.setMain()orDispatchers.resetMain()needed
Example:
class PokemonListViewModelTest : StringSpec({
lateinit var repository: PokemonListRepository
lateinit var testScope: TestScope
lateinit var viewModel: PokemonListViewModel
beforeTest {
repository = mockk(relaxed = true)
testScope = TestScope(StandardTestDispatcher())
viewModel = PokemonListViewModel(repository, testScope)
}
"loads pokemons successfully" {
coEvery { repository.loadPage(any(), any()) } returns Either.right(mockPage)
viewModel.uiState.test {
awaitItem() shouldBe PokemonListUiState.Loading
viewModel.loadInitialPage()
testScope.advanceUntilIdle()
awaitItem().shouldBeInstanceOf<PokemonListUiState.Content>()
.pokemons shouldHaveSize 20
cancelAndIgnoreRemainingEvents()
}
}
})
Workflow 3: Write Property-Based Tests with Kotest
MANDATORY: Before writing property tests, read property-testing-examples.md for complete patterns, custom Arb generators, and examples.
Quick checklist:
- Identify invariants (data preservation, transformation rules)
- Use
Arbgenerators:Arb.int(range),Arb.string(),Arb.list(), custom generators - Write with
checkAll()orforAll()(default 1000 iterations) - Name tests with "property:" prefix
- Balance: 30-40% property, 60-70% concrete tests
- Target: 100% property coverage for mappers
Critical Guardrails
| Rule | Consequence |
|---|---|
Tests MUST go in androidUnitTest/ for business logic |
Kotest/MockK unavailable on iOS/Native |
NEVER use Thread.sleep() or delay() in tests |
Use Turbine + testScope.advanceUntilIdle() |
| Mappers MUST have 100% property test coverage | Concrete tests insufficient for data invariants |
| 30-40% of tests MUST be property-based | Provides 1000× coverage multiplier |
| Every production file MUST have a test file | Tests are mandatory, not optional |
| 80% minimum coverage required per module | Fails CI if below threshold |
NO Dispatchers.setMain() with testScope |
Violates ViewModel pattern |
Quick Reference
Canonical Sources
- [@kmp-testing-strategy skill](@kmp-testing-strategy skill) — deep dive with rationale and playbooks
- @kmp-testing-patterns — concise pattern reminders
- [See @kmp-critical-patterns skill#testing-pattern](See @kmp-critical-patterns skill#testing-pattern) — canonical rules
- [See @kmp-critical-patterns skill](See @kmp-critical-patterns skill) — pattern cards view
Test Enforcement (NO CODE WITHOUT TESTS)
| Production Code | Location | Framework | Key Rule |
|---|---|---|---|
| Repository | androidUnitTest/ |
Kotest + MockK | Success + all error paths |
| ViewModel | androidUnitTest/ |
Kotest + MockK + Turbine | Initial, Loading, Success, Error + events |
| Mapper | androidUnitTest/ |
Kotest properties | Property tests proving data preservation |
@Composable |
Same file | @Preview (+ Roborazzi where used) |
Realistic preview + screenshot baseline |
| Simple Utility | commonTest/ |
kotlin-test | Pure functions only |
Flow Testing with Turbine
- Always test
Flow/StateFlow/SharedFlowwith Turbine - Prefer injecting a
TestScopevia constructor; NODispatchers.setMain/resetMain - Use
awaitItem(),skipItems(), andcancelAndIgnoreRemainingEvents()withadvanceUntilIdle()
Smart Casting
- Use Kotest contracts:
shouldBeRight { },shouldBeLeft { },shouldBeInstanceOf<>() - Avoid manual casts after assertions
- See @kmp-testing-patterns
Commands
| Command | Purpose |
|---|---|
./gradlew :composeApp:assembleDebug test --continue |
Build Android app + run all tests |
./gradlew test --continue |
Run all tests across all modules |
./gradlew :features:<feature>:testDebugUnitTest |
Run tests for specific feature module |
./.agents/skills/kmp-testing-strategy/scripts/test-coverage.sh [feature] |
Run tests + coverage for feature or all |
Implementation Examples
- PokemonListViewModelTest.kt - ViewModel test with Turbine
- PokemonDetailViewModelTest.kt - Property tests examples
- PokemonListRepositoryTest.kt - Repository test with MockK