Android Studio Journeys and Maestro let you describe a user flow in plain English — "sign in, add a plan to the cart, apply promo code SAVE10" — and an AI navigates the real app. That power is for exploration and coverage discovery. Your release gate stays Espresso and XCUITest, deterministic and boring.
Tier: Advanced · Workflow area: Testing & quality
This playbook splits mobile UI testing into two lanes with an explicit promotion rule between them, so natural-language journeys widen coverage without ever becoming the thing your release depends on.
Skip it if your app has fewer than five core flows — write the deterministic tests directly and move on.
maestro test working against your app.testTag, accessibilityIdentifier, resource-id) on key interactive elements. Both lanes depend on them.Inventory flows and sort them into lanes. List your user journeys in a table with three columns: flow, business criticality, and current deterministic coverage. Anything critical and uncovered is a promotion candidate; everything else is exploration territory. Have an assistant draft the inventory from your navigation graph or route definitions:
Here is our navigation graph / storyboard summary. List every user-reachable
flow as "start screen -> actions -> end state". Mark which flows touch
payment, auth, or data deletion — those are gate candidates.
Write journeys as intent, not clicks. In Journeys or Maestro, describe the goal and the assertions, not pixel coordinates:
# maestro: checkout_promo.yaml
appId: com.example.shop
---
- launchApp
- tapOn: "Sign in"
- inputText: "qa+journey@example.com"
- runFlow: flows/complete_login.yaml
- tapOn:
text: "Annual plan"
- tapOn: "Add to cart"
- inputText: "SAVE10"
- assertVisible: "10% discount applied"
In Android Studio Journeys, the same flow is a sentence per step; Gemini decides how to reach each goal. Let it improvise — that improvisation is the exploratory value.
Run the journey lane wide, and treat failures as leads. Run journeys across two or three device profiles (small phone, large phone, tablet) and both light/dark themes. A journey failure is a bug report or a coverage gap, never a red release. Log each failure with a screen recording — Maestro's maestro record or the Journeys run panel gives you this for free.
Promote with a rewrite, never a copy. When a journey proves a flow matters and is stable, write the deterministic version by hand or with AI assistance — Espresso/Compose testing APIs on Android, XCUITest on iOS — asserting on ids and state, with test data controlled by the test itself:
Convert this Maestro flow into an Espresso test using our
ComposeTestRule conventions (see CheckoutFlowTest.kt for style).
Replace text matchers with testTag matchers. The test must seed its
own cart state via TestDataRule — no dependence on prior tests.
Review the generated test for hidden nondeterminism: sleeps, text matchers on localized strings, and network dependence.
Enforce the lane boundary in CI. The deterministic suite runs on every PR and blocks merge. The journey lane runs nightly or pre-release, posts results to a channel, and is explicitly non-blocking. Write this down in the repo's testing doc so nobody "temporarily" gates a release on an AI-navigated flow.
Natural-language journeys are a discovery instrument: they find broken flows and coverage gaps cheaply. The moment a flow matters to release, it gets rewritten — not copied — into a deterministic Espresso or XCUITest that owns its data and asserts on identifiers.
for i in $(seq 10); do ./gradlew connectedCheck; done or the Xcode equivalent) before it may block merges.testTag/accessibilityIdentifier values. Make ids part of component-definition review.Client NDAs and regulated apps often forbid sending screen content to external models — and Journeys ships screenshots to Gemini.
A journey agent with a signed-in test account will happily tap "Delete account" or trigger real push notifications while exploring. Sandbox the account, stub outbound effects, and cap journey runs to staging builds.