Use different test layers for different risks. Unit tests protect pure logic and component contracts; end-to-end tests protect a few critical user journeys on a built app.
Unit and Component Tests
- Test observable behavior rather than component internals.
- Query by accessible role, label, and text where possible.
- Keep domain calculations and payload validation in pure functions.
- Mock the narrow native boundary, not the entire application.
- Cover loading, empty, error, success, disabled, and permission-denied states.
Jest tests should be deterministic and fast enough to run before every merge. Avoid snapshots so large that reviewers cannot see meaningful changes.
End-to-End Tests with Maestro
Write small flows around outcomes: sign in, complete onboarding, create the core object, recover from a failed request, and reach a purchased entitlement. Give interactive elements stable accessibility labels or test IDs only where semantic selectors are insufficient.
Control state with dedicated test accounts, seeded backend data, deep links, or reset endpoints. A flow that depends on another test's leftovers will eventually become flaky.
Run the Right Matrix
- Pull requests: types, lint, unit/component tests, and a small smoke flow.
- Main/release candidates: critical Maestro flows on both platforms.
- Before store submission: physical-device checks for notifications, purchases, biometrics, sharing, camera, and deep links.
Capture screenshots, logs, and the app build identifier when E2E tests fail. Retry only infrastructure failures; do not hide product flakes behind unconditional retries.
Sources
Use the current React Native testing overview, Jest getting-started guide, and Maestro documentation for runner and workflow details.