Practical Espresso testing for Android apps
Integration testing with Espresso in Android checks that screens behave correctly on a device or emulator. These instrumentation tests interact with controls, navigation, dialogs, and text fields, exposing faults that isolated unit tests may miss.
Place Espresso beside unit and API tests rather than above them. It confirms that UI events reach application logic, adapters, local storage, and network clients while remaining practical for Activities, Fragments, and classic Views.
Australian apps support busy commuters and widely used local services. A customer in Sydney may open a banking screen before a train journey, while someone in Melbourne may complete a transport purchase; both need clear state changes, useful error messages, and consistent behaviour across Android devices.
Why UI tests belong in Android projects
Unit tests are ideal for calculations, repositories, and data transformations. They run quickly but cannot confirm that a button is reachable, a form displays the correct validation message, or a navigation event opens the intended screen.
UI automation earns its place when these interactions form a critical user journey. Checkout, sign-in, account settings, and notification handling often involve several Android components, making them difficult to validate completely through isolated code tests.
Start with a small suite covering the workflows that would block customers or hide a production incident. A focused set of reliable tests provides more value than hundreds of brittle selectors that fail after minor layout changes.
Setting up the test environment
Espresso tests belong in the androidTest source set because they run inside an Android application process. Add the AndroidX Test dependencies to the app-level build file, using versions compatible with the project’s SDK and Gradle setup.
androidTestImplementation("androidx.test.ext:junit:1.2.1")
androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
Configure the instrumentation runner so Android Studio and Gradle can discover the tests.
defaultConfig {
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
For newcomers building their first test module, the practical Android examples on MyEasyProjects demonstrate how a small project can be organised before Espresso checks are added.
Writing focused Espresso tests
Each test should describe one observable outcome. Espresso locates a view, performs an action, and verifies the resulting state using matchers such as withId, withText, and isDisplayed.
@RunWith(AndroidJUnit4::class)
class LoginValidationTest {
@Test
fun shortPasswordShowsError() {
ActivityScenario.launch(LoginActivity::class.java)
onView(withId(R.id.password)).perform(typeText("123"))
onView(withId(R.id.sign_in)).perform(click())
onView(withText("Password must contain at least eight characters"))
.check(matches(isDisplayed()))
}
}
Prefer accessibility identifiers, stable resource IDs, and visible text over screen coordinates. When a view exists in a ListView or RecyclerView, scroll it into position before acting on it to avoid false failures caused by an off-screen item.
Keep test data isolated by resetting SharedPreferences, seeding a test SQLite database, or using a dedicated account. Tests should not depend on records left by another test, random network content, or a particular order of execution.
Handling dynamic screens and user flows
Dynamic content introduces timing and visibility problems. A progress indicator may disappear just after a test checks for it, or a dialog may appear only after a delayed database callback. Assertions should describe the final user-visible state rather than transient implementation details.
Swipeable screens need checks for both navigation and page indicators. The Android Open Tutorials guide to a ViewPager image slideshow offers a practical example that can be extended with Espresso swipe actions and text assertions.
Different tools cover different layers of the Android testing stack, so the right choice depends on how far the test must travel.
| Technique | Best suited to | Main advantage | Main limitation |
|---|---|---|---|
| Espresso | Views inside the app | Precise, direct UI interaction | Cannot interact with another application |
| Espresso with an idling resource | Asynchronous work inside the app | Coordinates background callbacks | Requires careful resource cleanup |
| UI Automator | System dialogs and cross-app flows | Reaches beyond the app boundary | Selectors are less specific |
| Compose UI Test | Jetpack Compose interfaces | Works with the semantics hierarchy | Requires a Compose-enabled test setup |
Use the narrowest suitable tool. Espresso provides clearer failures for standard Views, while UI Automator becomes valuable when the flow enters the system picker, permission screen, or another installed application.
Running tests from Android Studio
Android Studio can run a single method, an entire test class, or the connected test suite through the gutter icons beside test declarations. Running one example first makes it easier to distinguish a test defect from an emulator configuration problem.
The equivalent Gradle command is useful for repeatable local and remote execution.
./gradlew connectedDebugAndroidTest
For a product with several build variants, use the matching task, such as connectedFreeDebugAndroidTest, so the test APK and installed application share the same configuration.
A useful local matrix includes different Android API levels, common screen sizes, and both portrait and landscape orientations. Network calls should be deterministic, while a separate unmanaged-device run can expose problems that an emulator does not reproduce.
Synchronisation, rules, and reliable data
ActivityScenarioRule launches an Activity before a test and closes it afterward, preventing state from leaking into later examples. It provides a predictable lifecycle without requiring manual setup and teardown in every method.
@get:Rule
val activityRule = ActivityScenarioRule(LoginActivity::class.java)
Espresso waits for the main thread to become idle, but arbitrary background workers may continue independently. Register an IdlingResource for important network or database operations, and always unregister it so a failed test cannot affect the rest of the suite.
Australian products also need regional checks. Use en-AU, exercise time zones such as Australia/Sydney and Australia/Perth, and confirm that dates, currency, and error messages remain understandable. Major local services such as CBA, Woolworths, Australia Post, and transport operators make notification, offline recovery, and permission states especially important. Organisations covered by the Privacy Act 1988 must also align consent and data-access behaviour with the Australian Privacy Principles, while the Australian Consumer Law affects subscriptions, refunds, and pricing disclosures.
Building a continuous integration test pipeline
Continuous integration should install the correct application and test APKs, execute the connected test task, and preserve the generated reports as build artefacts. Managed virtual devices provide repeatable Android environments, while a small physical-device pool can validate manufacturer-specific behaviour such as notifications, biometrics, and camera permissions.
Large suites benefit from test sharding and Test Orchestrator, which can run classes independently and reduce accumulated device state. Splitting tests by stable feature or build variant keeps individual jobs short and makes failures easier to investigate.
Treat repeated failures as reliability defects rather than automatically increasing retries. Quarantine an unstable example, repair its selector, timing, or test data, and restore it once it produces consistent results. With that discipline, Espresso becomes a dependable quality gate across local development and automated delivery.