On This Page
1Overview2Testing Pyramid Layers
3Anti-Patterns to Avoid4References

Overview

Mobile testing has five layers in the pyramid. The base (unit tests) contains the most tests, runs the fastest, and provides the most actionable feedback. The apex (E2E tests on physical devices) contains the fewest tests, runs the slowest, and is reserved for smoke testing of critical production paths. Investment follows the pyramid: most engineering effort in unit tests, least in E2E.

Testing Pyramid Layers

Layer 1: Unit Tests (Foundation)

Tests Use Cases and ViewModels without any device, emulator, simulator, network, or database. Android: JUnit5 + Mockk (for mocking Kotlin interfaces) + Turbine (for testing Kotlin Flow/StateFlow emissions). iOS: XCTest + Swift Testing framework (Xcode 16) + mock protocols generated manually or with Mockingbird. Turbine's collectLatest {} test block allows asserting exact emission sequences from StateFlow. Coverage gates: Use Cases ≥ 90%, ViewModels ≥ 80%.

Layer 2: Snapshot Tests

Capture pixel-level screenshots of UI components and diff against committed golden images. Android: Paparazzi library renders Compose UI on the JVM using LayoutLib (same engine as Android Studio Preview) — no emulator required. iOS: iOSSnapshotTestCase renders SwiftUI or UIKit views to UIImage and diffs with stored PNG files. Test matrix: all theme variants (light, dark, high contrast), all supported Dynamic Type sizes (minimum: xSmall, medium, AX3), all localisation variants with different text lengths. Golden images committed to the repository — PRs that change visual appearance fail CI until goldens are intentionally updated.

Layer 3: Integration Tests

Test the Repository layer with its dependencies: in-memory Room database for Android, URLProtocol stubbing for network calls (iOS), in-memory SQLite for Core Data/SwiftData. Verify that data flows correctly from data source through mapping to domain model. These tests catch DTO-to-domain mapping bugs that unit tests (which mock the Repository) miss.

Layer 4: UI Tests

Full application on an emulator (Android) or simulator (iOS). Scope to the top 10 critical user journeys only — not every screen. Tools: Espresso (Android) with IdlingResource for async synchronisation; XCUITest (iOS) with accessibility identifier-based element location; Maestro (cross-platform, YAML-based) as an alternative that is more maintainable than programmatic Espresso. Maestro's declarative YAML syntax is readable by QA engineers without Kotlin/Swift knowledge.

Layer 5: E2E Tests (Apex)

Physical device matrix via Firebase Test Lab. 10-20 devices covering top Android OEM/version combinations. iOS: 5 physical device types. Scope: the 5 most critical user journeys (authentication, primary use case, payment if applicable, error recovery, push notification handling). Run on each release candidate, not on every PR.

Contract Tests (Cross-cutting)

Consumer-driven contract tests with Pact framework. The mobile application defines the API contract it depends on as a Pact file. CI verifies the BFF satisfies the contract before any backend deployment. Prevents backend changes from breaking the mobile app. Runs in the backend CI pipeline, not the mobile CI pipeline.

Anti-Patterns to Avoid

⚠ 1. Testing Only Through the UI

All tests use Espresso or XCUITest to drive the full application. Tests are slow (2-5 minutes each), brittle (fail on visual changes unrelated to the tested logic), and provide poor diagnostic information when they fail.

Hover to see the fix ↻
↺ Correct Approach

Test business logic through unit tests (milliseconds each, thousands of them). Test visual components through snapshot tests (seconds each). Reserve UI tests for the small set of critical user journeys that require the full stack.

⚠ 2. Coverage Theatre

80% code coverage achieved by testing getters, setters, and constructor wiring. No tests for actual business logic.

Hover to see the fix ↻
↺ Correct Approach

Coverage targets applied specifically to Use Cases and ViewModels — the layers that contain business logic. A 90% Use Case coverage target means 90% of the lines in Use Case classes are executed by tests. Coverage tooling configured to exclude auto-generated code, data classes without logic, and DI configuration.

Flowchart

%%{init:{'theme':'base','themeVariables':{'fontSize':'14px','fontFamily':'IBM Plex Sans, system-ui, sans-serif','primaryColor':'#DBEAFE','primaryTextColor':'#1e3a5f','primaryBorderColor':'#2563EB','lineColor':'#374151','clusterBkg':'#F9FAFB','clusterBorder':'#D1D5DB','edgeLabelBackground':'#FFFFFF'},'flowchart':{'curve':'orthogonal','padding':30,'nodeSpacing':65,'rankSpacing':75,'useMaxWidth':true}}}%% flowchart TD E2E["🔺 E2E Tests — Apex Firebase Test Lab · Physical devices Top 5 critical journeys Per release candidate only"] UI2["UI Tests Espresso · XCUITest · Maestro Top 10 user journeys Emulator / Simulator"] INTG2["Integration Tests Room in-memory · URLProtocol stub Repository + DataSource layer No device required"] SNAP2["Snapshot Tests Paparazzi · iOSSnapshotTestCase All components · All themes Dark · Light · All Dynamic Type sizes"] UNIT2["Unit Tests — Foundation JUnit5 + Mockk + Turbine XCTest + Swift Testing 90% Use Cases · 80% ViewModels Milliseconds per test"] CONTRACT["Consumer Contract Pact Framework Mobile defines API contract Backend CI verifies"] E2E --- UI2 --- INTG2 --- SNAP2 --- UNIT2 CONTRACT -.- INTG2 style E2E fill:#FFEBEE,stroke:#B71C1C style UI2 fill:#FFF3E0,stroke:#E65100 style INTG2 fill:#FFF9C4,stroke:#F57F17 style SNAP2 fill:#E8F5E9,stroke:#1B5E20 style UNIT2 fill:#E3F2FD,stroke:#1565C0 style CONTRACT fill:#F3E5F5,stroke:#6A1B9A

References

  1. Fowler, Martin — The Practical Test Pyramid. martinfowler.com/articles/practical-test-pyramid.html
  2. Google — Android Testing Guide. developer.android.com/training/testing
  3. Paparazzi — Android Snapshot Testing. github.com/cashapp/paparazzi
  4. Pact Foundation — Consumer-Driven Contracts. docs.pact.io
  5. Maestro — Mobile UI Testing. maestro.mobile.dev
Mobile Engineering Reference
← Mobile Development