For a React Native ticket-resale app's checkout flow, which checks go in Jest, RNTL and device E2E tests, and how do you keep each layer stable?
answer
- pricing and hold rules in pure Jest
- screen states in RNTL with mocked API
- payment sheet and deep link on device
- few E2E paths, stubbed backend
- countdown timers break sync and timing
basics
~20 sPut fee maths and seat-hold rules in Jest, each checkout screen state in RNTL against a mocked API, and one or two purchase paths plus native pieces such as the payment sheet in device E2E against a stubbed backend.
solid answer
~40 sSplit by what each layer can prove. **Jest**: pure rules such as resale fees, currency rounding, the seat-hold expiry and validation, with fake timers for the countdown. **RNTL**: each checkout state renders correctly: summary, disabled Pay button while submitting, card-declined and hold-expired messages, with the network and payment module mocked. **Device E2E**: one happy purchase path and one failure path per platform, covering what only a real build shows: the native payment sheet, the return deep link from the payment provider, the keyboard over the promo field. Stability: fake timers and awaited RNTL calls in Node; in E2E a stubbed backend and seeded tickets, analytics URLs excluded from synchronisation, and a countdown that does not keep the app busy forever.
code
tsx · 15 linesimport { fireEvent, render, screen } from '@testing-library/react-native';
import { CheckoutScreen } from '../src/checkout/CheckoutScreen';
import { payForOrder } from '../src/checkout/payments';
jest.mock('../src/checkout/payments');
test('keeps the form and explains a declined card', async () => {
jest.mocked(payForOrder).mockRejectedValue({ code: 'card_declined' });
await render(<CheckoutScreen orderId="ord_42" />);
await fireEvent.press(screen.getByRole('button', { name: 'Pay' }));
expect(await screen.findByText('Your card was declined')).toBeOnTheScreen();
expect(screen.getByRole('button', { name: 'Pay' })).toBeEnabled();
});go deeper
Recall the three layers and that checkout maths belongs in fast Jest tests while the full purchase belongs on a device.
Map each checkout check to the cheapest layer that can see it, and explain why native pieces like the payment sheet need a real build.
Name the flake sources per layer for this flow, such as timers, unawaited RNTL calls, live services and a countdown blocking Detox, and the fix for each.
Decide how often each layer runs and on which platforms, so checkout confidence stays high without making every pull request wait on device builds.
## The flow under test A buyer picks resale tickets for a concert, sees a price breakdown (face value, resale fee, service fee, taxes), gets a **seat hold** with a visible countdown, enters a promo code, pays through a **native payment sheet**, may be sent to the payment provider and returned through a **deep link**, and lands on an order confirmation. The plan decides which check lives where, so each bug is caught by the cheapest layer that can see it. ## Layer 1: pure Jest tests Anything that is a function of inputs belongs here: - fee and tax calculation, including rounding per currency; - promo-code rules (expired, single-use, minimum basket); - seat-hold state: when a hold expires, what happens at exactly zero, what happens if the server extends it; - mapping payment-API error codes to user-facing reasons. These run in milliseconds and are deterministic. The countdown logic uses **fake timers**, so tests never wait on the wall clock. ## Layer 2: component tests with React Native Testing Library Each screen state is rendered with the network client and the payment module mocked: 1. the summary shows the breakdown from a fixed quote; 2. the Pay button is disabled while a submission is in flight and re-enabled on failure; 3. a declined card shows the right message and keeps the entered data; 4. an expired hold replaces the form with a "hold expired" state; 5. a missing native payment module (mocked as rejecting) shows a fallback. With RNTL 14, `render` and events are asynchronous, so every call is awaited; unawaited calls are a classic source of flaky component tests. ## Layer 3: device E2E Only what needs the real app goes on a simulator or device: - **Happy path**: pick seats, pay with a test card, see the confirmation. Once per platform. - **Failure path**: card declined, then retried successfully. - **Native-only checks** folded into those paths: the payment sheet opens and returns, the provider's return **deep link** reopens the right screen, the promo field is reachable with the keyboard open on a small screen. React Native's testing guide recommends exactly this focus: vital flows such as payments on E2E, the rest in faster JavaScript tests. ## Where flakiness comes from, per layer | Layer | Typical flake source | Fix | |---|---|---| | Jest | Countdown or retry logic on real timers | Fake timers; advance explicitly | | RNTL | Unawaited async `render` or events; shared mock state between tests | Await every call; reset mocks and storage in `beforeEach` | | Device E2E | Real payment or ticket services; seat inventory changing between runs | Stub backend or a dedicated test environment with seeded tickets | | Device E2E with Detox | A one-second countdown built on repeated `setTimeout`, or an endless loader, keeps the app "busy" | Build the countdown so it does not block idle (Detox ignores `setInterval` by default), or mock it through Metro | | Device E2E | Analytics or WebSocket traffic that never settles | Exclude those URLs from Detox's network synchronisation | | Any E2E tool | Cold simulator boots, animations, device state from earlier runs | Reuse booted devices, reset app state per test, keep the suite short | ## Run cost - Jest and RNTL run on every commit in seconds to minutes, on any CI machine. - E2E needs a **native build per platform** plus a booted simulator or emulator; it is minutes per run even for two flows. Run it on merges to the main branch and before releases, or on every pull request only if the budget allows. - A failing E2E test costs triage time. Two reliable flows are worth more than twenty that fail at random. ## What the plan deliberately leaves out - Every fee combination on a device: the maths is already proven in Jest. - Snapshot-testing whole checkout screens: they change constantly and hide which assertion matters. - Testing the payment provider itself: the contract is stubbed; the provider is outside the app. A senior answer shows the mapping, justifies it by what each layer can see, and names the specific flake sources a checkout with a countdown, network calls and a native payment sheet will hit.
- Why test the seat-hold expiry in Jest rather than waiting for it in a device test?The expiry is a rule about time, so a device test would have to wait out the real countdown or manipulate the clock from outside, which is slow and flaky. In Jest, fake timers move the clock instantly and cover edge cases such as expiry at exactly zero. The device test only needs to show that the hold-expired screen can appear.
- The Detox checkout test hangs on the seat-hold screen until it times out; what is the likely cause?The countdown re-arms a short `setTimeout` every second, which Detox's synchronisation treats as pending work, so the app never becomes idle. Detox ignores `setInterval` by default, so implementing the tick with an interval, or mocking the countdown through Metro in the test build, lets synchronisation settle.
saying these in an interview costs you the question
- Every fee combination should be verified on a real device
- Component tests with a mocked payment module prove the payment sheet works
- The E2E suite should call the real payment and ticket services
- More E2E tests always mean more confidence at the same cost
- Countdown timers are harmless in Detox because it ignores all timers