Skip to content
All posts
February 26, 20264 min read

The Automation Pyramid in Practice: A Real Android App Example

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.

AndroidTestingAutomationKotlin
Share:

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.


The App: A Task Manager

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.


Layer 1: Unit Tests (Foundation — 60-70% of tests)

Unit tests cover individual functions, classes, and logic in complete isolation. No Android framework, no database, no network.

What gets unit-tested:

  • ViewModels (business logic, state management)
  • Domain use cases
  • Data mappers and transformations
  • Utility functions
kotlin
@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


Layer 2: Integration Tests (Middle — 20-30% of tests)

Integration tests verify that components work together: ViewModel + Repository + Room database, or API client + network layer.

What gets integration-tested:

  • Repository implementations (real Room DB)
  • API client with mock HTTP server
  • Data flow from repository to ViewModel
kotlin
@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:

kotlin
@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


Layer 3: UI Tests (Top — 5-10% of 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:

  • Critical user journeys (create task → complete task → delete task)
  • Navigation flows
  • Error state handling that requires real UI
kotlin
@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


The Numbers in Practice

LayerToolCountAvg DurationTotal Time
UnitJUnit + MockK120~15ms~2s
IntegrationJUnit + Room40~200ms~8s
UIEspresso15~8s~2 min
Total175~2.5 min

A 2.5-minute test suite that runs on every PR. This is achievable.


CI Configuration

yaml
# 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 connectedAndroidTest

Unit and integration tests run everywhere, fast. UI tests run on macOS runners with emulators — slower but reserved for the important flows.


What I Skip at Each Layer

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


Takeaways

  • Unit tests: 60-70%, fast, isolated, cover all business logic
  • Integration tests: 20-30%, verify database and network interactions work
  • UI tests: 5-10%, cover critical journeys only — not every screen
  • Target total suite under 5 minutes — longer and engineers stop running it
  • Run unit + integration on every commit; UI tests on PRs or scheduled CI runs
Share:
S

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.

Stay updated

Get new posts on Android, Kotlin, and solo dev straight to your inbox.

Newsletter preferences

Related Apps

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.

Building something? Available for Android dev and QA consulting.

Work with me

Comments — powered by Giscus

Apps tagged with this