testing-android
| Type | Skill |
| Plugin | awl-testing · v0.0.27 |
| Invoke | /awl-testing:testing-android |
| Tools | Read, Write, Edit, Bash, Glob, Grep |
| Source | plugins/awl-testing/skills/testing-android/SKILL.md |
When Claude uses it
Abschnitt betitelt „When Claude uses it“Writes Kotlin unit tests using Kotest, Turbine for Flows, and Fakes. Writes Compose UI tests using Robolectric, Robot pattern, and Roborazzi screenshots. Use when “write kotlin tests”, “kotest test”, “turbine test”, “compose test”, “robolectric test”, “roborazzi screenshot”, “test viewmodel”, “test android”, “create fake”, “test flow”, “test stateflow”, “robot pattern”, “fix android test”, “debug kotlin test”.
Trigger phrases: write kotlin tests · kotest test · turbine test · compose test · robolectric test · roborazzi screenshot · test viewmodel · test android · create fake · test flow · test stateflow · robot pattern · fix android test · debug kotlin test
Definition
Abschnitt betitelt „Definition“Quick Start
Abschnitt betitelt „Quick Start“@Testfun login_validCredentials_returnsSuccess() = runTest { // Arrange val dispatcher = UnconfinedTestDispatcher() val viewModel = LoginViewModel( dispatchers = AppDispatchers(default = dispatcher, io = dispatcher), authHandler = FakeAuth(result = AppResult.Success(Unit)), )
// Act & Assert with Turbine viewModel.state.test { awaitItem() // Initial viewModel.login("user", "pass") awaitItem().isSuccess.shouldBeTrue() }}Run tests:
./gradlew :shared:testAndroidHostTest./gradlew :androidApp:testDevDebugUnitTest --tests "*MyFeatureTest*"When to Use
Abschnitt betitelt „When to Use“| Task | Approach |
|---|---|
| ViewModel/Repository | Unit test + Turbine |
| Same logic, many inputs | Burst parameterized test (see references/unit-testing.md) |
| Compose screen | Robolectric + Robot pattern |
| Visual regression | Roborazzi screenshots in a *ScreenshotTest class via captureScreenshot |
| Coroutines/Flows | UnconfinedTestDispatcher + Turbine |
Essential Rules
Abschnitt betitelt „Essential Rules“1. Always Inject Dispatchers
Abschnitt betitelt „1. Always Inject Dispatchers“ViewModels take AppDispatchers from com.appswithlove.kmp.coroutines; BaseViewModel builds catchingCoroutineScope from it.
class MyViewModel( dispatchers: AppDispatchers,) : BaseViewModel(dispatchers)
// In testval dispatcher = UnconfinedTestDispatcher()val vm = MyViewModel(dispatchers = AppDispatchers(default = dispatcher, io = dispatcher))2. Set Main Dispatcher in Compose UI Tests
Abschnitt betitelt „2. Set Main Dispatcher in Compose UI Tests“collectAsStateWithLifecycle uses Dispatchers.Main internally. Without setting it, the flow never collects and the test hangs indefinitely. Use a custom rule:
class MainDispatcherRule( val dispatcher: TestDispatcher = UnconfinedTestDispatcher(),) : TestWatcher() { override fun starting(description: Description) { Dispatchers.setMain(dispatcher) } override fun finished(description: Description) { Dispatchers.resetMain() }}Apply in every Compose UI test that uses collectAsStateWithLifecycle:
@RunWith(RobolectricTestRunner::class)class MyScreenTest { @get:Rule val mainDispatcherRule = MainDispatcherRule()
@get:Rule val composeTestRule = createComposeRule()
@Test fun screen_showsContent() { composeTestRule.setContent { // collectAsStateWithLifecycle now works MyScreen(viewModel = testViewModel) } // assertions... }}Recommend adding a custom rule in your project’s test utilities so every test class can simply add
@get:Rule val mainDispatcherRule = MainDispatcherRule().
3. Use Turbine for StateFlow
Abschnitt betitelt „3. Use Turbine for StateFlow“@Testfun state_updates() = runTest { viewModel.state.test { awaitItem() // Initial viewModel.doAction() awaitItem().data shouldBe expected }}4. Fakes Over Mocks
Abschnitt betitelt „4. Fakes Over Mocks“class UserProvidingFake : UserProviding, UserHandling { val users = MutableStateFlow<List<User>>(emptyList()) var saveResult: AppResult<Unit> = AppResult.Success(Unit)
override fun observeUsers(): Flow<List<User>> = users override suspend fun save(user: User): AppResult<Unit> = saveResult.onSuccess { users.update { it + user } }
fun reset() { users.value = emptyList(); saveResult = AppResult.Success(Unit) }}
// Failure casefake.saveResult = AppResult.Failure(AppError.Network)5. Robot Pattern for UI
Abschnitt betitelt „5. Robot Pattern for UI“@Testfun addItem_showsInList() { launchItemListScreen(composeTestRule) { clickAddButton() typeItemName("New Item") clickSave() } verify { itemIsDisplayed("New Item") }}Naming Convention
Abschnitt betitelt „Naming Convention“methodName_condition_expectedResult
Examples:
login_withValidCredentials_returnsSuccessstate_whenLoaded_containsItemsdeleteItem_onError_showsSnackbar
Common Assertions (Kotest)
Abschnitt betitelt „Common Assertions (Kotest)“result shouldBe expectedlist shouldContainExactly listOf(a, b, c)list.shouldBeEmpty()condition.shouldBeTrue()result.shouldBeInstanceOf<Success>()nullable.shouldNotBeNull()Turbine Essentials
Abschnitt betitelt „Turbine Essentials“flow.test { awaitItem() // Get next emission awaitItem().data shouldBe expected // Assert on emission cancelAndIgnoreRemainingEvents() // Cleanup}Robot Pattern
Abschnitt betitelt „Robot Pattern“fun launchMyScreen( rule: ComposeContentTestRule, block: MyRobot.() -> Unit,): MyRobot = MyRobot(rule).apply { rule.waitForIdle(); block() }
class MyRobot(private val rule: ComposeContentTestRule) { fun clickButton() = rule.onNodeWithTag("button").performClick() infix fun verify(block: Verification.() -> Unit) = Verification(rule).apply(block)
class Verification(private val rule: ComposeContentTestRule) { fun itemDisplayed(name: String) = rule.onNode(hasText(name)).assertIsDisplayed() }}Troubleshooting
Abschnitt betitelt „Troubleshooting“Test Hangs (No Emissions)
Abschnitt betitelt „Test Hangs (No Emissions)“// BAD: Flow never emitsviewModel.state.value shouldBe expected
// GOOD: Use TurbineviewModel.state.test { awaitItem() shouldBe expected}collectAsStateWithLifecycle Hangs Forever
Abschnitt betitelt „collectAsStateWithLifecycle Hangs Forever“// BAD: Missing Main dispatcher — flow never collects, test hangs indefinitelycomposeTestRule.setContent { MyScreen(viewModel) }
// GOOD: Add MainDispatcherRule so collectAsStateWithLifecycle can dispatch@get:Rule val mainDispatcherRule = MainDispatcherRule()Dispatcher Issues
Abschnitt betitelt „Dispatcher Issues“// BAD: Hardcoded dispatcherprivate val scope = CoroutineScope(Dispatchers.Main)
// GOOD: Injected AppDispatchers, scope provided by BaseViewModelclass MyViewModel( dispatchers: AppDispatchers,) : BaseViewModel(dispatchers) { fun load() = catchingCoroutineScope.launch { }}StateFlow WhileSubscribed Not Emitting
Abschnitt betitelt „StateFlow WhileSubscribed Not Emitting“// WhileSubscribed needs active collectorviewModel.state.test { // Now subscribed, flow starts awaitItem()}Robolectric Test Fails
Abschnitt betitelt „Robolectric Test Fails“# Check SDK version in test config@Config(sdk = [33])
# Ensure correct test runner./gradlew :androidApp:testDevDebugUnitTestCompose Node Not Found
Abschnitt betitelt „Compose Node Not Found“// BAD: Node may not exist yetrule.onNodeWithTag("button").performClick()
// GOOD: Wait for idlerule.waitForIdle()rule.onNodeWithTag("button").performClick()Anti-Patterns
Abschnitt betitelt „Anti-Patterns“// BAD: Using runBlocking@Testfun test() = runBlocking { } // Use runTest
// BAD: Not using TurbineviewModel.state.value shouldBe expected // Won't work with WhileSubscribed
// BAD: Mocks for everythingval mock = mockk<Repository>() // Prefer Fakes
// BAD: Navigation in every testrule.onNodeWithTag("home").performClick()// Use Robot composition insteadRunning Tests
Abschnitt betitelt „Running Tests“# Shared module (JVM host, includes commonTest)./gradlew :shared:testAndroidHostTest./gradlew :shared:testAndroidHostTest --tests "*MyClassTest*"./gradlew :shared:iosSimulatorArm64Test
# Android unit + Robolectric UI tests (dev flavor)./gradlew :androidApp:testDevDebugUnitTest./gradlew :androidApp:testDevDebugUnitTest --tests "*MyFeatureTest*"
# Screenshots; classes named *ScreenshotTest run only under a Roborazzi task./gradlew recordRoborazziDevDebug --tests "*MyFeatureScreenshotTest*"./gradlew verifyRoborazziDevDebug
# Coverage./gradlew koverPrintCoverageCustomProjects with other flavors swap Dev for their flavor name in the task.
PR Checklist
Abschnitt betitelt „PR Checklist“- All tests use
UnconfinedTestDispatcherviaAppDispatchers(default = it, io = it) - Repeated cases use a Burst parameterized test instead of copy-pasted tests
- Compose UI tests include
MainDispatcherRule(forcollectAsStateWithLifecycle) - Flows tested with Turbine
- Fakes reset in
@BeforeTest - UI tests use Robot pattern
- Test names follow
method_condition_result - No hardcoded strings (use test data objects)
References
Abschnitt betitelt „References“For advanced patterns and complete examples:
- unit-testing.md - Fakes, Kotest, Turbine, coroutines
- ui-testing.md - Robolectric, Robot pattern, Roborazzi
- troubleshooting.md - Common failures and solutions

