From android
Comprehensive Android unit testing skill enforcing MockK, coroutines-test, Turbine, and best practices for writing reliable, maintainable tests.
How this skill is triggered — by the user, by Claude, or both
Slash command
/android:unit-testThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
This skill provides authoritative rules and patterns for writing production-quality unit tests in Android/Kotlin projects. It enforces MockK, kotlin-coroutines-test, Turbine, and modern testing best practices.
This skill provides authoritative rules and patterns for writing production-quality unit tests in Android/Kotlin projects. It enforces MockK, kotlin-coroutines-test, Turbine, and modern testing best practices.
Activate this skill when the user asks to:
runTest, TestDispatcher) for all coroutine testing. Do NOT use runBlocking.toList() or custom collectors.assert calls are fine if they verify the same behavior.if, for, when, or try-catch blocks.@BeforeEach (JUnit 5) or @Before (JUnit 4) for common setup, not init blocks.@Test
fun `should emit loading then success when data is fetched`() { ... }
@Test
fun `should throw exception when user id is empty`() { ... }
@Nested inner classes (JUnit 5) or clear naming prefixes.com.app.feature.MyViewModel → com.app.feature.MyViewModelTest.testFixtures or test source set, not in production code.mockk<T>() for creating mocks. Use spyk() only when you need to call real methods.relaxed = true sparingly — prefer explicit stubbing so tests fail fast on unexpected calls.// CORRECT — explicit stubbing
private val repository = mockk<UserRepository>()
init {
coEvery { repository.getUser(any()) } returns testUser
}
// AVOID — relaxed hides missing stubs
private val repository = mockk<UserRepository>(relaxed = true)
coEvery / coVerify for suspend functions, every / verify for regular functions.slot<T>() and capture() to assert on arguments passed to mocked functions.confirmVerified() when you need to ensure no unexpected interactions occurred.@Test
fun `should call repository with correct parameters`() = runTest {
val idSlot = slot<String>()
coEvery { repository.getUser(capture(idSlot)) } returns testUser
useCase.execute("user-123")
assertEquals("user-123", idSlot.captured)
coVerify(exactly = 1) { repository.getUser(any()) }
}
mockkStatic or mockkObject unless absolutely necessary (e.g., extension functions). Prefer refactoring to inject dependencies.runTest for testing coroutine code. NEVER use runBlocking.runTest provides virtual time and auto-advances delays.@Test
fun `should load data successfully`() = runTest {
coEvery { repository.getData() } returns testData
viewModel.loadData()
advanceUntilIdle()
assertEquals(UiState.Success(testData), viewModel.uiState.value)
}
TestDispatcher into classes under test. Never rely on real dispatchers.StandardTestDispatcher for precise control over execution order.UnconfinedTestDispatcher when you need eager/immediate execution.@Test
fun `should switch to IO dispatcher`() = runTest {
val testDispatcher = StandardTestDispatcher(testScheduler)
val repository = UserRepository(ioDispatcher = testDispatcher)
repository.fetchUser()
advanceUntilIdle()
// assertions
}
advanceUntilIdle() to process all pending coroutines.advanceTimeBy(millis) to test delay-based logic (e.g., debounce, retry).Thread.sleep() or real delay() in tests..test { } to collect and assert Flow emissions.awaitItem().cancelAndIgnoreRemainingEvents() or ensureNoMoreEvents().@Test
fun `should emit loading then success`() = runTest {
viewModel.uiState.test {
assertEquals(UiState.Loading, awaitItem())
assertEquals(UiState.Success(testData), awaitItem())
cancelAndIgnoreRemainingEvents()
}
}
awaitError() to assert error emissions.awaitComplete() for finite flows that complete.@Test
fun `should emit error when network fails`() = runTest {
coEvery { repository.getData() } throws IOException("Network error")
viewModel.loadData()
viewModel.errorFlow.test {
val error = awaitItem()
assertIs<IOException>(error)
cancelAndIgnoreRemainingEvents()
}
}
awaitItem() or explicitly skip with skipItems(n)..test(timeout = 5.seconds) { }.Clock or time providers for time-dependent logic.TestDispatcher for coroutine timing — never rely on real thread scheduling.testUser, emptyResponse, networkError — not data1, obj.companion object {
private val testUser = User(id = "1", name = "John", email = "[email protected]")
private val emptyUserList = emptyList<User>()
private val networkError = IOException("Connection refused")
}
equals, hashCode, copy, toString — Kotlin generates these.When generating tests, produce:
Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.
npx claudepluginhub canergulgec/mobile-dev-skills --plugin android