A practical reference project for writing reliable, maintainable end-to-end tests for Android apps built with Jetpack Compose.
Companion source code for the article: "E2E Testing for Android with Jetpack Compose: A Practical Guide"
- End-to-end testing of a full login flow using the Compose testing API
- Page Object Model (POM) for readable, maintainable test code
- MockWebServer for deterministic network stubbing — no real API calls in tests
- Custom JUnit rules for mock server lifecycle and on-failure screenshots
- CI/CD pipeline with GitHub Actions running tests on an Android emulator
| Layer | Technology |
|---|---|
| Language | Kotlin |
| UI | Jetpack Compose + Material 3 |
| Navigation | Navigation Compose |
| Architecture | MVVM (ViewModel + StateFlow) |
| Testing | Compose UI Test, AndroidJUnit4, UiAutomator |
| Network mocking | OkHttp MockWebServer |
| CI | GitHub Actions |
SDK targets: minSdk = 30, targetSdk = 36, compileSdk = 36
app/src/
├── main/
│ └── java/com/example/e2etestdemo/
│ ├── MainActivity.kt
│ ├── navigation/
│ │ └── AppNavigation.kt # NavHost with login → home routes
│ ├── data/
│ │ ├── AuthRepository.kt # Interface
│ │ ├── DefaultAuthRepository.kt # HTTP implementation (dummyjson.com)
│ │ └── RepositoryProvider.kt # Global singleton for DI in tests
│ └── ui/
│ ├── TestTags.kt # Centralized semantic tags
│ ├── login/
│ │ ├── LoginScreen.kt
│ │ ├── LoginViewModel.kt
│ │ └── LoginUiState.kt
│ └── home/
│ └── HomeScreen.kt
│
└── androidTest/
└── java/com/example/e2etestdemo/
├── LoginFlowTest.kt # E2E test suite (5 test cases)
├── pages/
│ ├── LoginPage.kt # Page Object for LoginScreen
│ └── HomePage.kt # Page Object for HomeScreen
├── rules/
│ ├── MockServerRule.kt # Starts/stops MockWebServer per test
│ └── ScreenshotRule.kt # Captures screenshot on failure
└── utils/
├── MockResponses.kt # Canned HTTP responses
└── ComposeTestExtensions.kt # waitUntilExists helper
| ID | Test | Scenario |
|---|---|---|
| TC-001 | successfulLogin_navigatesToHomeWithUsername |
Valid credentials → navigates to Home |
| TC-002 | emptyUsername_showsValidationError |
Empty username → client-side error |
| TC-003 | emptyPassword_showsValidationError |
Empty password → client-side error |
| TC-004 | wrongCredentials_showsServerErrorMessage |
Server returns 401 → "Invalid credentials" |
| TC-005 | networkFailure_showsGenericError |
Server closes connection → network error message |
Tests read as user stories, not UI queries:
loginPage
.enterUsername("emilys")
.enterPassword("emilyspass")
.clickLogin()
homePage.assertWelcomeMessage("emilys")Network is fully controlled — no flakiness from real APIs:
@get:Rule(order = 0) val mockServer = MockServerRule()
mockServer.server.enqueue(MockResponses.loginSuccess("emilys"))MockServerRule(0) → server starts, RepositoryProvider wired to localhost
ComposeRule(1) → Activity launches, reads RepositoryProvider
ScreenshotRule(2) → wraps test; captures screenshot on failure
# Run all E2E tests on a connected device or emulator
./gradlew connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.size=largeTest reports are saved to:
app/build/reports/androidTests/connected/
Failure screenshots are saved to the device at:
/sdcard/Android/data/com.example.e2etestdemo/files/screenshots/
Pull them with:
adb pull /sdcard/Android/data/com.example.e2etestdemo/files/screenshots/Tests run automatically on every push and pull request to main via .github/workflows/android-e2e.yml.
- Emulator: API 34, Pixel 6 profile, x86_64
- Artifacts uploaded on every run: test reports
- Artifacts uploaded on failure: failure screenshots
MIT