MyFamilyTracker
Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.
The automation testing pyramid is easy to draw on a whiteboard and hard to apply to a real codebase. Here's how it maps to an actual Android app with concrete test counts and tool choices.
On this page
Every QA article shows the testing pyramid. Few show what it looks like in an actual project with real numbers, real tools, and real trade-offs.
Here's how I structure testing for an Android app.
Simple scope to keep examples concrete: a task manager with local Room database, ViewModel with StateFlow, Hilt for DI, and a Compose UI. Users can create, complete, and delete tasks.
Real-world complexity: network sync to a backend API, authentication, notifications.
Unit tests cover individual functions, classes, and logic in complete isolation. No Android framework, no database, no network.
What gets unit-tested:
@Test
fun `completing a task updates its status in state`() = runTest {
val repository = FakeTaskRepository()
val viewModel = TaskViewModel(repository)
val task = Task(id = "1", title = "Buy groceries", completed = false)
repository.addTask(task)
viewModel.completeTask("1")
val state = viewModel.uiState.value
assertThat(state.tasks.first { it.id == "1" }.completed).isTrue()
}Tools: JUnit 5, MockK, Turbine (for Flow testing), kotlinx-coroutines-test
Target count for this app: ~120 unit tests
Integration tests verify that components work together: ViewModel + Repository + Room database, or API client + network layer.
What gets integration-tested:
@Test
fun `creating a task persists it in the database`() = runTest {
val db = Room.inMemoryDatabaseBuilder(
ApplicationProvider.getApplicationContext(),
AppDatabase::class.java
).build()
val repository = TaskRepositoryImpl(db.taskDao())
val task = Task(id = "new-1", title = "Write tests", completed = false)
repository.createTask(task)
val saved = repository.getTask("new-1")
assertThat(saved).isNotNull()
assertThat(saved!!.title).isEqualTo("Write tests")
}For API integration, use MockWebServer (OkHttp) to simulate the backend:
@Test
fun `sync sends local tasks to server`() = runTest {
mockServer.enqueue(MockResponse().setBody("""{"synced": 3}"""))
syncService.syncPendingTasks(localTasks)
val request = mockServer.takeRequest()
assertThat(request.path).isEqualTo("/api/tasks/sync")
}Tools: JUnit 4 (Robolectric/Instrumentation), Room in-memory DB, MockWebServer, Hilt testing
Target count for this app: ~40 integration tests
UI tests run on a real device or emulator. They're slow, but they verify the actual user experience end-to-end.
What gets UI-tested:
@Test
fun `user can create and complete a task`() {
// Create task
composeTestRule.onNodeWithTag("add_task_button").performClick()
composeTestRule.onNodeWithTag("task_title_input").performTextInput("Buy milk")
composeTestRule.onNodeWithTag("save_task_button").performClick()
// Verify task appears
composeTestRule.onNodeWithText("Buy milk").assertIsDisplayed()
// Complete task
composeTestRule.onNodeWithText("Buy milk").performClick()
composeTestRule.onNodeWithTag("complete_button").performClick()
// Verify completed state
composeTestRule.onNodeWithTag("task_item_Buy milk")
.assertHasClickAction()
.assertIsEnabled()
}Tools: Espresso, Compose Testing (composeTestRule), UI Automator for system interactions
Target count for this app: ~15 UI tests
| Layer | Tool | Count | Avg Duration | Total Time |
|---|---|---|---|---|
| Unit | JUnit + MockK | 120 | ~15ms | ~2s |
| Integration | JUnit + Room | 40 | ~200ms | ~8s |
| UI | Espresso | 15 | ~8s | ~2 min |
| Total | 175 | ~2.5 min |
A 2.5-minute test suite that runs on every PR. This is achievable.
# GitHub Actions
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Run unit + integration tests (JVM)
run: ./gradlew test
ui-test:
runs-on: macos-latest # Required for Android emulator
steps:
- name: Start emulator
run: |
avdmanager create avd -n test -k "system-images;android-34;google_apis;x86_64"
emulator -avd test -no-window -no-audio &
adb wait-for-device
- name: Run UI tests
run: ./gradlew connectedAndroidTestUnit and integration tests run everywhere, fast. UI tests run on macOS runners with emulators — slower but reserved for the important flows.
Skip in unit tests: Android SDK classes, database, network — use fakes/mocks
Skip in integration tests: Full UI rendering, end-user journeys — use unit mocks for ViewModel
Skip in UI tests: Negative cases, edge cases, error codes — test those at lower layers
Sudarshan Chaudhari
AI Systems Builder / Product Engineer
Bangkok, Thailand
Solo Android developer with 13+ years in QA, building Android apps, AI automation systems, and developer tools at SudarshanTechLabs.
Related Posts
Related Apps
Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.
Building something? Available for Android dev and QA consulting.
Work with meComments — powered by Giscus
Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.
ReadPrivate dream journal — structured entry capture, pattern tagging, and optional Claude-powered insight generation. All data stays on-device by default.
ReadWorkout tracker — exercise logging with set/rep/weight history, goal progression, and local Room DB persistence. No account, no cloud sync required.
Read