| name | mobiai-android-testing |
| description | Use when writing or running tests in an Android project — unit tests, UI tests, choosing the right framework and patterns. |
| license | MIT |
| compatibility | ["claude-code","cursor","copilot","codex"] |
| platforms | ["android"] |
Android Testing
Comprehensive guide to writing and running tests in Android projects.
When to Use
- Writing unit tests for Android/Kotlin code
- Setting up test infrastructure
- Running and debugging test suites
- Choosing the right testing approach
Test Types
| Type | Location | Framework | Speed | Uses Device |
|---|
| Unit | src/test/ | JUnit + MockK | Fast | No |
| Integration | src/test/ | JUnit + Robolectric | Medium | No (simulated) |
| UI / Instrumented | src/androidTest/ | Espresso / Compose Testing | Slow | Yes |
Unit Testing (JUnit + MockK)
Setup
dependencies {
testImplementation("junit:junit:4.13.2")
testImplementation("io.mockk:mockk:1.13.12")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0")
testImplementation("app.cash.turbine:turbine:1.2.0")
}
File Location
Mirror the source path:
- Source:
app/src/main/java/com/example/feature/MyViewModel.kt
- Test:
app/src/test/java/com/example/feature/MyViewModelTest.kt
Test Structure
class MyViewModelTest {
@get:Rule
val mainDispatcherRule = MainDispatcherRule()
private val repository: MyRepository = mockk()
private lateinit var viewModel: MyViewModel
@Before
fun setUp() {
viewModel = MyViewModel(repository)
}
@Test
fun `loadData sets state to Success when repository returns data`() = runTest {
val expected = listOf(Item("1", "Test"))
coEvery { repository.getData() } returns expected
viewModel.loadData()
assertEquals(UiState.Success(expected), viewModel.state.value)
coVerify(exactly = 1) { repository.getData() }
}
@Test
fun `loadData sets state to Error when repository throws`() = runTest {
coEvery { repository.getData() } throws IOException("Network error")
viewModel.loadData()
assertTrue(viewModel.state.value is UiState.Error)
}
}
MainDispatcherRule (for ViewModel tests)
class MainDispatcherRule(
private val dispatcher: TestDispatcher = UnconfinedTestDispatcher()
) : TestWatcher() {
override fun starting(description: Description) {
Dispatchers.setMain(dispatcher)
}
override fun finished(description: Description) {
Dispatchers.resetMain()
}
}
MockK Cheat Sheet
val repo: Repository = mockk()
val repo: Repository = mockk(relaxed = true)
every { repo.getData() } returns listOf(item)
coEvery { repo.fetchData() } returns result
every { repo.getData() } throws IOException()
every { repo.save(any()) } just runs
verify { repo.getData() }
verify(exactly = 1) { repo.save(any()) }
coVerify { repo.fetchData() }
verify(exactly = 0) { repo.delete(any()) }
val slot = slot<String>()
every { repo.search(capture(slot)) } returns emptyList()
mockkObject(MySingleton)
every { MySingleton.instance } returns mockk()
Flow Testing with Turbine
@Test
fun `events flow emits navigation event`() = runTest {
viewModel.events.test {
viewModel.onSubmitClicked()
assertEquals(Event.NavigateToSuccess, awaitItem())
cancelAndConsumeRemainingEvents()
}
}
Compose UI Testing
Setup
dependencies {
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
debugImplementation("androidx.compose.ui:ui-test-manifest")
}
Test Structure
class MyScreenTest {
@get:Rule
val composeRule = createComposeRule()
@Test
fun submitButton_isDisabled_whenFieldIsEmpty() {
composeRule.setContent {
MyScreen(state = MyState(text = ""))
}
composeRule.onNodeWithText("Submit").assertIsNotEnabled()
}
@Test
fun errorMessage_isDisplayed_whenStateIsError() {
composeRule.setContent {
MyScreen(state = MyState(error = "Something went wrong"))
}
composeRule.onNodeWithText("Something went wrong").assertIsDisplayed()
}
}
Espresso (XML Views)
@Test
fun clickSubmit_showsConfirmation() {
onView(withId(R.id.btn_submit)).perform(click())
onView(withText("Confirmed")).check(matches(isDisplayed()))
}
Running Tests
./gradlew testDebugUnitTest
./gradlew test<Flavor>DebugUnitTest
./gradlew testDebugUnitTest --tests "com.example.MyViewModelTest"
./gradlew testDebugUnitTest --tests "com.example.MyViewModelTest.loadData sets state to Success*"
./gradlew connectedDebugAndroidTest
Common Patterns
Testing Use Cases
@Test
fun `invoke returns transformed data`() = runTest {
coEvery { repository.getItems() } returns rawItems
val result = useCase()
assertEquals(expectedTransformed, result)
}
Testing Repository with Room
val db = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java).build()
val dao = db.myDao()
Testing Mapper/Converter Functions
@Test
fun `toEntity maps all fields correctly`() {
val dto = MyDto(id = "1", name = "Test", value = 42.0)
val entity = dto.toEntity()
assertEquals("1", entity.id)
assertEquals("Test", entity.name)
assertEquals(42.0, entity.value, 0.001)
}
Troubleshooting
Tests pass locally but fail in CI
- Check timezone-dependent tests
- Check tests that depend on system locale
- Check flaky tests with
@FlakyTest or increase timeouts
MockK issues with final classes
Add to src/test/resources/mockk-extensions/io.mockk.ifc:
io.mockk.classmocking.enabled=true
Coroutine test timing issues
Use advanceUntilIdle() or runCurrent() in runTest blocks.