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