End to End Testing
End to end testing - Implement end-to-end testing for Expo/React Native apps in 2026. Guide covers Detox, Playwright, setup, and stable tests for a reliable

Your React Native app probably already has tests. Maybe you've got solid unit coverage around hooks and utility functions. Maybe a few integration tests cover navigation, form validation, or your auth client. Then a release goes out and a basic flow breaks anyway. The login button renders, the API works, and each piece passed in isolation, but the actual path a user takes on a real device fails.
That's where end to end testing earns its keep.
In Expo projects, this gets more confusing than it should. Most testing advice still assumes a web app, a browser, and a DOM. That's not the world you're shipping in. You're dealing with dev client builds, simulators, native permissions, Metro, Expo config, and often a backend stack that includes Supabase and a lightweight API layer like Hono. You need tooling that respects mobile reality, not a browser-first workaround.
Table of Contents
- Why Your Expo App Needs End to End Testing
- Setup Guide From Zero to a Running Test with Detox
- Writing Stable and Maintainable E2E Tests
- Integrating E2E Tests into Your CI/CD Pipeline
- Advanced Strategies and Final Recommendations
Why Your Expo App Needs End to End Testing
The bug that slips past everything else
The most expensive mobile bugs aren't usually deep algorithm mistakes. They're broken journeys. A returning user opens the app, taps sign in, completes the form, and gets stuck on a loading state because the token refresh and navigation handoff don't behave correctly on a real device. Unit tests won't catch that. Integration tests often won't either.
End to end testing checks the whole path as a user experiences it. The app launches, screens render, buttons get tapped, text gets entered, network requests happen, and the app either completes the flow or fails in front of you. That's why it's the last safety net before release.
A flowchart explaining why Expo apps require end-to-end testing to address limitations in unit and integration testing.
If you're already tightening your broader release process, AppLighter's guide to mobile app quality assurance practices is a useful companion to this work.
E2E tests don't prove every line of code is correct. They prove your most important user journeys still work when all the moving parts meet.
Use E2E as the top layer, not the whole strategy
A common mistake is swinging too far and trying to automate everything through the UI. That creates a slow, fragile suite that people stop trusting. A better model is the testing pyramid. A mature suite keeps end-to-end tests at 10% to 20% of total tests, while unit tests make up 60% to 70% according to Virtuoso's explanation of the testing pyramid.
That ratio matters because E2E tests are the slowest and most expensive tests to run. They exercise the full stack and give you confidence that isolated tests cannot provide, but they should stay focused on business-critical workflows.
For an Expo app, that usually means flows like these:
- Authentication: sign up, sign in, session restore, sign out
- Core value path: create a record, submit content, complete onboarding, send a message
- Revenue path: subscription, purchase confirmation, gated feature access
- High-risk state transitions: offline recovery, deep links, permission prompts, token expiry
The practical rule is simple. If a broken flow would block users, cost you support time, or break trust, it deserves end to end coverage. If it's a rendering detail or a pure function, keep it lower in the pyramid.
Choosing Your E2E Testing Tool for React Native
What matters in an Expo workflow
For Expo and React Native, tool choice isn't about who has the longest feature list. It's about who creates the least friction when the app is native. The criteria I care about are straightforward:
- Native awareness: can it drive iOS and Android like mobile apps, not pretend they're websites
- Expo compatibility: does it work cleanly with dev client or custom builds
- Debugging quality: when a test fails, can the team tell whether the issue is the app, the environment, or the test
- CI fit: can you run it repeatably in a pipeline without babysitting simulators all day
Tool comparison
| Criterion | Detox | Playwright | Cypress + Appium |
|---|---|---|---|
| Native app focus | Strong native-first fit for React Native | Primarily web-first, with mobile relevance in adjacent workflows | Appium is mobile-capable, but the stack feels layered and heavier |
| Expo setup experience | Good once you commit to a dedicated test build | Better suited to web or hybrid testing needs | Usually more moving parts to wire together |
| Debugging React Native UI issues | Strong for app-level interactions and test IDs | Excellent for browser workflows, less natural for pure native app debugging | Can work, but the developer loop is often slower |
| Simulator and emulator workflow | Fits local device automation well | Less direct for native mobile app control | Supported through Appium, but setup complexity rises |
| CI/CD ergonomics | Good when builds and devices are standardized | Strong in browser CI, less opinionated for Expo native apps | Usable, but maintenance overhead is higher |
| Recommendation for Expo apps | Best default choice | Secondary option for mixed web concerns | Only choose if you already have strong Appium experience |
My recommendation
For a modern Expo app, Detox is the default choice unless you have a very specific reason to do something else.
Playwright is excellent software, but it shines in browser automation. If your product has a heavy web surface and your team already lives in Playwright, it may deserve a place in the broader test strategy. It usually isn't the cleanest first answer for a native Expo app.
Cypress plus Appium is workable, but I almost never recommend it for a team starting fresh. The pieces can be made to work, but the integration tax is real. You'll spend more time wiring infrastructure than protecting critical mobile flows.
Selection rule: pick the tool that matches the runtime your users touch. For Expo and React Native, that means a native-first tool.
Detox isn't magic. You still need a stable build, explicit test IDs, and disciplined environment setup. But it lines up with how React Native apps behave, and that saves time over the life of the project.
Setup Guide From Zero to a Running Test with Detox
Start with a testable Expo build
The first mindset shift is this: Detox doesn't test Expo Go. It tests your app binary. In Expo projects, that usually means using a dev client or another custom native build that includes what Detox needs to launch and control the app.
A developer typing on a keyboard at a wooden desk with a laptop showing code and coffee.
If your project is still early, it helps to get the native side straight before adding test automation. AppLighter's primer on getting started with Expo is useful if you need a quick reset on the build model.
For local development, I prefer a dedicated E2E build profile. Keep it separate from day-to-day dev builds so you can control environment flags, API endpoints, and debug features without contaminating the normal developer experience.
A typical approach looks like this:
- Create a build profile for E2E in your Expo config.
- Point it at test-safe services and credentials.
- Enable deterministic app startup behavior.
- Remove any first-run screens or feature flags that would make tests inconsistent.
Install Detox and configure the project
Once the build strategy is clear, install Detox and your test runner. Many teams use Jest because the surrounding ecosystem is familiar.
A practical project shape often includes:
- A Detox dependency in
devDependencies - A dedicated
e2efolder for tests and helpers - A config file describing app binaries and simulator targets
- NPM scripts for build, test, cleanup, and device selection
Your config needs to answer three questions:
- Which binary should Detox install?
- Which device should it run on?
- Which test runner should execute the suite?
For iOS, start with one simulator model and one runtime. For Android, start with one emulator profile. Don't chase matrix coverage on day one. The fastest way to fail with end to end testing is trying to support every device before you've made one path reliable.
Here's the shape I like conceptually:
- Apps: one iOS binary path, one Android binary path
- Devices: a named iPhone simulator and a named Android emulator
- Configurations: local iOS, local Android, and later CI variants
- Behavior: explicit launch arguments for E2E mode if the app needs them
Practical rule: standardize the simulator name in the repo docs. Half of “Detox is flaky” complaints are actually “everyone ran a different local environment.”
You'll also want a tiny bit of app-side support. Add testID props to key controls now, not after the first failures. In React Native, that means things like email-input, password-input, login-submit, and home-screen.
Write your first real test
Your first test should prove the setup, not impress anyone. Login is usually the right choice because it touches UI input, a network call, app state, and navigation.
A simple flow looks like this in practice:
- Launch the app with a clean state.
- Wait for the login screen to appear.
- Type an email into the email field.
- Type a password into the password field.
- Tap the sign-in button.
- Assert that the next screen appears.
That flow sounds small. It isn't. It validates that your build boots, Detox can target elements, your environment is reachable, and the app can complete a meaningful transition.
Later in the setup, this walkthrough is worth watching because it helps teams connect the config files to the actual execution loop:
A few implementation choices will save you pain:
- Use dedicated test credentials: don't depend on shared accounts people also use manually
- Reset state between tests: avoid flows that assume leftovers from a previous run
- Assert on destination state: check for a stable screen identifier, not a transient toast
- Keep the first test boring: boring tests are easier to debug
Run it locally and debug the right thing
When the first run fails, don't assume Detox is the problem. In Expo projects, failure usually comes from one of four places:
- The binary is wrong: built for the wrong profile or missing expected native config
- The simulator is inconsistent: stale app data, wrong runtime, or an already-busy device
- The environment is unstable: API endpoint mismatch, expired credentials, or missing seed data
- The selector is weak: text-based targeting that breaks on copy changes
I like to debug in this order:
- Launch the app manually on the same simulator.
- Confirm the expected screen appears without the test runner.
- Verify the exact
testIDvalues in the UI code. - Re-run only the failing test.
- Capture logs before changing assertions.
If you do that consistently, most early failures stop feeling mysterious. They become ordinary engineering work, which is exactly what you want.
Writing Stable and Maintainable E2E Tests
Selectors decide whether your suite survives
Most E2E pain is self-inflicted. Teams write a few promising tests, then the suite starts failing because someone changed a class name, moved text, or updated a layout wrapper. That's why selector strategy matters more than almost anything else.
Brittle selectors are a primary cause of test fragility, and guidance from CircleCI's end-to-end testing article recommends data-attribute selectors, which map naturally to testID in React Native. The same source also notes that network flakiness and environment mismatches cause 60% to 75% of E2E failures in production-like pipelines in these scenarios, which is exactly why mobile teams need disciplined test boundaries and environment control in the first place, as described in CircleCI's guide to end-to-end testing.
An infographic titled Strategies for Stable E2E Tests, listing six best practices for reliable test automation.
In practical React Native terms, that means this:
- Use
testIDfor intent, not styling:profile-save-buttonis good.blue-button-rightis not. - Avoid visible text as the main selector: copy changes often, especially in onboarding and marketing-driven screens.
- Target stable containers: assert that a screen root exists before interacting with child elements.
- Name IDs like public API: if your test suite depends on them, treat them as contract surface.
Structure tests so failures are readable
A failing test should answer one question fast: what broke? If every spec contains raw selectors, repeated login steps, and scattered setup logic, that answer takes too long to find.
I prefer a lightweight Page Object pattern. Not enterprise ceremony. Just enough structure so common actions live in one place and screen-level behavior reads clearly.
For example:
- A
LoginScreenhelper knows how to fill credentials and submit. - A
HomeScreenhelper knows how to assert the signed-in state. - Test files describe scenarios, not low-level button hunting.
Keep abstractions thin. If the helper hides too much, debugging gets harder instead of easier.
The maintainability payoff is real. When the login screen changes, you update one helper instead of rewriting assertions across the suite.
Handle Supabase and other external dependencies carefully
Many mobile E2E setups get messy. You want realism, but you don't want tests to fail because an outside service had a bad minute. The answer usually isn't full mocking. That removes the very integration risk you're trying to catch.
For a stack using Supabase and Hono, the better pattern is controlled realism:
- Use a dedicated Supabase project or isolated schema for tests
- Seed predictable users and records before the run
- Clean up or reset data after execution
- Call your own Hono endpoints in a test environment configured for repeatability
- Reserve true third-party live calls for a very small number of smoke tests
The reason to be strict here is simple. E2E suites degrade when external dependency handling is vague. Mobile apps feel this more sharply because device automation already adds enough moving pieces.
Another source of flakiness is environment drift. Growth Acceleration Partners notes that staging environments that mirror production more closely, along with refreshed or snapshotted test data after execution, reduce failures caused by non-code issues. They also call out brittle selectors and implicit waits as recurring pitfalls in E2E testing in their article on avoiding common E2E testing pitfalls.
A few rules work well in practice:
- Don't use arbitrary sleeps: wait for visible, interactive state
- Don't share mutable test accounts across the team: tests should own their data
- Don't cover every edge case through the UI: push non-critical branches down to lower-level tests
Integrating E2E Tests into Your CI/CD Pipeline
What to automate first
Running Detox locally is useful. Running it automatically on pull requests is what turns it into a release safeguard.
The first CI target shouldn't be your entire imagined suite. Start with a narrow gate:
- One platform first: usually iOS in GitHub Actions if your team already uses macOS runners
- A handful of critical flows: login, onboarding completion, and one primary in-app action
- Deterministic environment setup: same binary, same simulator profile, same seeded data
That gives you signal without creating a pipeline everyone tries to bypass.
A row of black server racks in a modern data center with flashing green and blue lights.
If you already ship with code push style workflows or staged rollouts, it also helps to think about how E2E fits beside release velocity. AppLighter's article on over-the-air updates for mobile apps pairs well with that operational side of the conversation.
A practical GitHub Actions shape
Your workflow file needs to do a few jobs reliably:
- Check out the repository and install dependencies.
- Restore caches where useful.
- Build or download the app binary intended for E2E.
- Boot the simulator or emulator.
- Run Detox against the named configuration.
- Upload logs, screenshots, and artifacts when things fail.
A simple YAML shape often includes separate steps for JavaScript dependencies, native tooling, simulator boot, and test execution. Keep those steps explicit. When CI is too clever, debugging slows down.
A few implementation habits matter more than the exact syntax:
- Pin the device target: don't let CI choose any available simulator
- Keep secrets minimal: only expose what the E2E environment needs
- Fail with artifacts attached: screenshots and logs save hours
- Separate build from test when possible: that makes retries cheaper
Keep CI failures debuggable
The worst CI setup is the one that produces a red checkmark and no useful evidence. A good E2E pipeline leaves behind a trail.
Store at least these artifacts on failure:
- Detox logs
- App logs if available
- Screenshots at the failure point
- Video, if your runner setup supports it
- Test report output in a readable format
A failed mobile E2E run without artifacts is just a rumor.
Also decide what blocks merging. I prefer making a small, trusted suite mandatory and keeping broader regression coverage non-blocking until the team has proven it's stable. That avoids the familiar pattern where a flaky gate teaches everyone to ignore automation.
Advanced Strategies and Final Recommendations
Where to go after the first stable suite
Once the basics are reliable, a few upgrades become worth exploring. Visual regression checks can catch unintended UI changes that functional assertions won't notice. Native permission dialogs deserve dedicated handling because camera, notifications, and location prompts often behave differently from ordinary screens. Performance-oriented checks can also be layered into selected flows so the team notices obvious regressions before users do.
Accessibility should stay in the conversation too. Even if your primary app is React Native, it helps to study how other ecosystems evaluate usability and interface stability. DOM Studio's roundup of accessibility tools for Vue developers is a good example of thinking beyond pure happy-path automation and toward product quality as a whole.
Final recommendations
If you're setting up end to end testing for an Expo app today, the practical stack is straightforward. Use Detox. Create a dedicated test build. Add stable testID values early. Keep tests focused on user-critical paths. Control your data and external dependencies instead of pretending the network will always behave.
Don't try to prove everything through the UI. That's how teams build large suites nobody wants to maintain. A smaller suite with disciplined selectors, predictable environments, and strong CI artifacts does more for release confidence than a huge pile of brittle tests.
End to end testing has an upfront cost. It also saves time by catching the kind of failures users notice, on the platforms they use.
If you want an Expo foundation that already includes the hard production pieces, AppLighter gives you a fast starting point for building and shipping React Native apps with auth, backend integrations, and an opinionated structure that's ready for real-world delivery.