android-unit-testing
Skill18 starsChanged 4 months agoMITView on GitHub
Unit testing for Android - JUnit5, AssertK, Turbine, fakes over mocks, UnconfinedTestDispatcher, SavedStateHandle, dispatcher injection, exhaustive edge-case enumeration, and an optional kotlin-mutation-testing pass (run only when explicitly requested). A unit test runs entirely in-process, in memory, with NO I/O - no disk, no network, no database, no real clock, no real concurrency. It can wire many real classes together as long as none of them cross those boundaries. Use this skill whenever writing or reviewing tests for ViewModels, validators, mappers, use cases, formatters, reducers, or any pure-logic unit. Trigger on phrases like "unit test", "test the ViewModel", "test this logic", "fake repository", "Turbine", "UnconfinedTestDispatcher", "runTest", "JUnit5", "test the validator", "test the mapper", or "test the use case".
1 other project uses a version of this.
Adopted by
Most recently taken up by
- LeeYoonSam/ClaudeSkillsForAndroidtheir own version, first seen Nov 19, 2025
Other versions1 project uses a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 1
- LeeYoonSam/ClaudeSkillsForAndroid★ 0same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository67 other files it gives its agents.
- agent-onboardingSkill
- android-backgroundSkill
- android-cliSkill
- android-compose-architectureSkill
- android-compose-componentsSkill
- android-data-layerSkill
- android-di-koinSkill
- android-error-handlingSkill
- android-integration-testingSkill
- android-jacoco-setupSkill
- android-jvm-test-coverageSkill
- android-module-structureSkill
- android-navigationSkill
- android-presentation-mviSkill
- android-preview-figma-verifySkill
- android-ui-screenshot-verifySkill
- android-ui-testingSkill
- android-ui-verifySkill
- android-version-catalogSkill
- architecture-decision-recordSkill
- careful-planningSkill
- compose-adaptive-layoutsSkill
- figma-to-android-vectorSkill
- finalizeSkill
- git-commitSkill
- handoffSkill
- kmp-compose-architectureSkill
- kmp-compose-componentsSkill
- kmp-data-layerSkill
- kmp-di-koinSkill
- kmp-error-handlingSkill
- kmp-integration-testingSkill
- kmp-migration-analyzerSkill
- kmp-module-structureSkill
- kmp-navigationSkill
- kmp-presentation-mviSkill
- kmp-ui-testingSkill
- kmp-unit-testingSkill
- kotlin-coroutinesSkill
- kotlin-flowsSkill
- kotlin-mutation-testingSkill
- openspec-artifact-review-lightSkill
- openspec-artifact-reviewSkill
- project-breakdownSkill
- react-data-layerSkill
- react-error-handlingSkill
- react-integration-testingSkill
- react-presentation-mviSkill
- react-project-structureSkill
- react-routingSkill
- react-screen-architectureSkill
- react-securitySkill
- react-server-stateSkill
- react-ui-componentsSkill
- react-unit-testingSkill
- speckit-parallelize-agent-teamsSkill
- speckit-parallelize-subagentsSkill
- spring-api-layerSkill
- spring-di-configurationSkill
- spring-integration-testingSkill
- The first 60 shown.
The file
We split this skill into the rules it gives an agent
10 rules. About 0 are common, 7 are distinctive, and 3 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈7
Distinctive · estimate
This author's own choices. Worth a look.
3
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 53
Write a test for each break vector you can name.
- Distinctive: line 66
Stop only when the next test you can name would re-test something already covered.
- Distinctive: line 68
If you want to prove you stopped at the right point, an optional mutation-testing pass (see Optional verification) is the strongest check — …
- Distinctive: line 135
Always prefer fakes for repository, data-source, and service collaborators.
- Very specific: line 201
Only inject CoroutineDispatcher into a class when both are true:
- Very specific: line 208
If a non-ViewModel class uses withContext(Dispatchers.IO) and is unit-tested, inject:
- Distinctive: line 225
When you do run it:
- Very specific: line 228
Run kotlin-mutation-testing end-to-end on the test class.
- Distinctive: line 230
Read the "why the test missed it" line.
- Distinctive: line 235
If a mutation survives because the production behavior it changes is not observable through the public API, the answer is usually that the …
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Android Unit Testing
What is a unit test (sharp definition)
A unit test:
- Runs entirely in-process, in memory.
- Does no I/O — no disk, no network, no database, no real system clock (use a fake
Clock), no real concurrency primitives. - Is independent of every other test, runs in milliseconds.
- May wire as many real classes together as is convenient, as long as none of them cross those boundaries.
Examples:
- ✅
LoginViewModel+ realLoginValidator+ realEmailNormalizer+ fakeAuthRepository→ unit. - ❌
LoginViewModel+ realAuthRepositoryoverMockWebServer→ integration (network boundary). - ❌
UserDao+ in-memory Room → integration (real SQLite engine).
When to switch to a different skill
- The test crosses an I/O / process / external-state boundary (Room, MockWebServer, real
Clock, file system, WorkManager) → use android-integration-testing. - The test exercises Compose UI on a device → use android-ui-testing.
Stack
| Concern | Library |
| Test framework | JUnit5 |
| Assertions | AssertK |
| Flow / StateFlow | Turbine |
| Coroutines | kotlinx-coroutines-test + UnconfinedTestDispatcher |
How to enumerate the test cases for a unit
A unit test suite is complete when you cannot name another way to break the unit without changing its public contract. Don't stop at the happy path.
Procedure:
- List the unit's public surface — every
fun, every observable state field, every emitted event, every return-value branch. For every observable behavior, ask:
"How could we possibly break this behavior?"
Write a test for each break vector you can name.
Walk this default checklist for every unit:
- Empty input.
- Single-element input.
- Large / repeated input.
- Null / optional fields, default values.
- Each error branch from every collaborator (success, every
DataError, exception). - Action dispatched while the unit is already busy (loading, in-flight network, debounce window).
- Idempotency: same action twice in a row.
- Loading → success → failure transitions, in every order.
- State restoration from
SavedStateHandleafter process death. - Boundary values for any numeric / range logic (0, 1, max, max+1, negative).
- Stop only when the next test you can name would re-test something already covered.
If you want to prove you stopped at the right point, an optional mutation-testing pass (see Optional verification) is the strongest check — run it when the user explicitly asks.
ViewModel test setup
class NoteListViewModelTest {
private val testDispatcher = UnconfinedTestDispatcher()
@BeforeEach
fun setUp() {
Dispatchers.setMain(testDispatcher)
}
@AfterEach
fun tearDown() {
Dispatchers.resetMain()
}
}UnconfinedTestDispatcher runs coroutines eagerly inline — no need to call advanceUntilIdle() after every action.
Testing state with Turbine
@Test
fun `loading notes emits loading then notes`() = runTest {
val repo = FakeNoteRepository().apply {
notes = listOf(Note("1", "Hello"))
}
val viewModel = NoteListViewModel(repo)
viewModel.state.test {
assertThat(awaitItem().notes).isEmpty()
viewModel.onAction(NoteListAction.OnRefreshClick)
assertThat(awaitItem().isLoading).isTrue()
val loaded = awaitItem()
assertThat(loaded.isLoading).isFalse()
assertThat(loaded.notes).hasSize(1)
}
}Testing events (one-time side effects)
@Test
fun `clicking note emits NavigateToDetail`() = runTest {
val viewModel = NoteListViewModel(FakeNoteRepository())
viewModel.events.test {
viewModel.onAction(NoteListAction.OnNoteClick("123"))
assertThat(awaitItem()).isEqualTo(NoteListEvent.NavigateToDetail("123"))
}
}Fakes over mocks
Always prefer fakes for repository, data-source, and service collaborators. Reach for mocks only when faking is impossible (e.g., final framework class you can't subclass, and even then prefer a thin wrapper you control).
A fake is a working in-memory implementation of the real interface:
class FakeNoteRepository : NoteRepository {
var notes = listOf<Note>()
var shouldReturnError = false
override suspend fun getNotes(): Result<List<Note>, DataError.Local> {
return if (shouldReturnError) Result.Error(DataError.Local.UNKNOWN)
else Result.Success(notes)
}
override suspend fun insertNote(note: Note): EmptyResult<DataError.Local> {
notes = notes + note
return Result.Success(Unit)
}
}Why fakes win:
- Tests assert behavior (final state, emitted values), not call patterns.
- Refactoring the production interface doesn't silently break tests because the fake refuses to compile.
- One fake serves dozens of tests; mocks usually duplicate setup per test.
Mocks (MockK) are acceptable only when:
- The collaborator is a final framework class you cannot subclass and don't own.
- You're verifying a side effect that has no observable result (rare — push back on this).
A unit is not a single class
The "unit" is a unit of logical behavior, not a single file. A unit test of LoginViewModel may instantiate the real LoginValidator, real EmailNormalizer, and real PasswordRulesProvider — all of them are pure logic and stay within the unit-test boundaries. Don't extract a fake just to satisfy an imaginary "one class per test" purity rule.
Rule of thumb: if a collaborator is pure logic and cheap to construct, use the real thing. Only fake collaborators that would cross an I/O boundary.
Instrumented unit tests are still unit tests
If a class needs Android framework types but does no I/O (e.g., a pure helper around Uri parsing), it can live under androidTest/ and still be a unit test by this skill's definition. Don't force it onto the JVM by adding shadows or wrappers — run it instrumented.
This is rare. Most genuine units are JVM-only. But when a unit truly needs Android, an instrumented unit test is fine.
SavedStateHandle in tests
Instantiate it directly with a map — no mocking:
val savedStateHandle = SavedStateHandle(mapOf("noteId" to "123"))
val viewModel = NoteEditorViewModel(savedStateHandle, FakeNoteRepository())To verify state restoration after process death, set values on the handle, build the ViewModel, and assert the recovered state.
When to inject dispatchers
Only inject CoroutineDispatcher into a class when both are true:
- The class dispatches to a non-Main dispatcher (e.g.,
withContext(Dispatchers.IO) { … }). - The class is directly unit-tested.
ViewModels that only use viewModelScope do not need an injected dispatcher — Dispatchers.setMain() covers them.
If a non-ViewModel class uses withContext(Dispatchers.IO) and is unit-tested, inject:
class ImageEncoder(private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) {
suspend fun encode(bytes: ByteArray): String = withContext(ioDispatcher) { … }
}
// In test:
val encoder = ImageEncoder(ioDispatcher = UnconfinedTestDispatcher())Optional verification — mutation testing
Once a class's @Test methods are written and green, a mutation-testing pass is the strongest way to prove the suite from How to enumerate the test cases is actually complete — it breaks the production code and checks the suite catches the break. It is not run automatically: invoke the kotlin-mutation-testing skill on the test class only when the user explicitly asks for a mutation test (or you want a deeper confidence check and the user is on board). Writing ordinary unit tests does not require it.
When you do run it:
- Confirm the production files the test exercises are clean in git (
kotlin-mutation-testingrequires this for safe restore). - Run
kotlin-mutation-testingend-to-end on the test class. - For every survived mutation in the report:
- Read the "why the test missed it" line.
- Either revise the offending test so the mutation would now be killed, or add a new test that catches that break vector.
- Re-run
kotlin-mutation-testingon just the revised tests until every mutation is killed (or correctly skipped per that skill's rules). - Report to the user: which tests were healthy on first pass, which needed revision, and what the revisions covered.
If a mutation survives because the production behavior it changes is not observable through the public API, the answer is usually that the unit needs a new public observation point — not that the test should be loosened.
Anti-patterns
| Anti-pattern | What to do instead |
Asserting on mock verify { … } call patterns | Use a fake and assert on the resulting state or emission. |
delay() or Thread.sleep() in tests | Use UnconfinedTestDispatcher and Turbine. |
Two @Test methods sharing mutable state on the test class | Make state local to each test or reset in @BeforeEach. |
Real Dispatchers.IO / Default reaching production code under test | Inject a test dispatcher per When to inject dispatchers. |
| Fake silently swallows exceptions and returns success | Fakes should mirror real failure modes; expose a shouldReturnError toggle. |
| Testing private fields via reflection | Test through the public surface; if a behavior isn't observable, it shouldn't have a test. |
| Stopping the test suite at the happy path | Walk the edge-case checklist before declaring done; a mutation pass is an optional deeper check when requested. |
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

