Entry_01 // Mar '26

The E2E Rabbit Hole: Mapping the User Journey.

Internal_Memo //
"Unit tests check the spark plugs; E2E ensures the car actually survives the test drive."

This entry chronicles the transition from isolated unit testing to holistic end-to-end (E2E) validation. The focus shifted from function-level verification to simulating the 'User Journey,' treating the UI as a black box. Key milestones included navigating the synchronization complexities of native Android testing and establishing a rigid 'Page Object Model' to ensure the test suite remains maintainable even as the XML layouts evolve.

I spent the better part of today digging into End-to-End (E2E) testing for my Kotlin app. I've been comfortable with unit tests for a while, but realizing that a single passed function doesn't mean the entire user journey works was a bit of a wake-up call.

Here's what I've gathered so far in my "study session":

The Big Realization: It's All About the "Journey"

Unit tests feel like checking if the spark plugs work; E2E feels like actually taking the car for a high-speed test drive. I learned that for a Kotlin app, I'm essentially treating the UI as a Black Box. I shouldn't care how the code fetches the data—only that the "Welcome" screen actually pops up after the user hits "Login."

My Toolkit Options

Espresso

The "gold standard." Native Kotlin integration with superior UI synchronization.

UI Automator

For the "messy" stuff—system dialogs and notifications outside the app window.

Maestro

YAML-based and fast. A lifesaver for rapid "happy path" test coverage.

The Data Dilemma: Handling APIs

If my E2E test hits the real production API, I'm asking for trouble. I've narrowed down two ways to handle this 'Data Flakiness':

01

The 'MockWebServer' Approach

Scripting local responses on localhost:8080 to test both Success (200 OK) and Failure (500 Error) states.

02

Dagger/Hilt Injection

Swapping real services for a FakeApiService in the androidTest folder for 100% offline testing.

Important 'Field Notes'

  • IDLING RESOURCES: Implement these to handle loading spinners; never let Espresso guess when background work is finished.
  • TEST ORCHESTRATOR: Enable in build.gradle to clear storage between runs and prevent session leakage.
  • THE 'WAIT' RULE: Never use Thread.sleep(). Brittle tests are useless tests. Use library-specific waits.

The Strategy: 'Page Object Model' (POM)

Instead of hunting for IDs in every test, I'm creating 'Screen' classes (like LoginScreen.kt). If an ID changes in the XML, I update it once in the Page Object, and 50 tests stay green. Clean code matters even in testing.