1. Jest Config & Mocking Native Modules
npx expo install jest-expo jest @testing-library/react-native --dev
{
"scripts": { "test": "jest" },
"jest": {
"preset": "jest-expo",
"transformIgnorePatterns": [
"node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@sentry/react-native|native-base|react-native-svg)"
]
}
}
jest-expo ships with mocks for most Expo native modules already —
calling Notifications.scheduleNotificationAsync in a test doesn't try
to talk to a real device; it resolves against a mock. A module without a built-in
mock needs one added manually in a jest.mock() call, pointing at a
fake implementation matching its real shape.
2. React Native Testing Library
import { render, screen, fireEvent } from '@testing-library/react-native';
import CartScreen from './CartScreen';
test('removing an item removes it from the list', () => {
render(<CartScreen />);
expect(screen.getByText('Wireless Mouse x1')).toBeOnTheScreen();
fireEvent.press(screen.getByText('Wireless Mouse x1'));
expect(screen.queryByText('Wireless Mouse x1')).not.toBeOnTheScreen();
});
RNTL's philosophy, carried over from its web counterpart: query the screen the way a user would find something — by visible text, a role, or an accessibility label — not by internal component names or implementation details a refactor could change without actually breaking anything real.
3. Testing Hooks & Async Code
import { render, screen, waitFor } from '@testing-library/react-native';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import PostsScreen from './PostsScreen';
function renderWithProviders(ui: React.ReactElement) {
const queryClient = new QueryClient();
return render(
<QueryClientProvider client={queryClient}>{ui}</QueryClientProvider>
);
}
test('shows posts once loaded', async () => {
renderWithProviders(<PostsScreen />);
expect(screen.getByTestId('loading-spinner')).toBeOnTheScreen();
await waitFor(() => {
expect(screen.getByText('First Post')).toBeOnTheScreen();
});
});
waitFor polls its callback until it stops throwing (or times out) —
the correct way to assert on anything that resolves asynchronously, rather than a
fixed setTimeout that's either too slow or flaky under load.
4. Snapshot Tests: When They Help
A snapshot test records a component's rendered output and fails if it changes —
cheap to write, but a large snapshot diff on every unrelated change trains a team
to blindly run jest --updateSnapshot without reading it, which defeats
the point.
- Good fit: small, stable, presentational components (a badge, a price formatter) where "did the output change at all" is exactly the right question.
- Poor fit: whole screens, or anything that changes often for legitimate reasons — an assertion like
getByText('First Post')stays meaningful there; a full-screen snapshot mostly just tracks churn.
5. Hands-on Exercise
Add a real test suite to an earlier screen
Write unit and component tests covering rendering, interaction, and async data for a screen from this course.
Requirements:
- Configure Jest with
jest-expoand React Native Testing Library in the project from an earlier week. - A component test for a list screen (e.g. the cart or notes screen) covering initial render and at least one user interaction (press, remove, toggle).
- An async test for a screen using TanStack Query (Week 10), asserting the loading state renders first and the loaded content appears after
waitFor. - A test for a custom hook (e.g.
useIsOnlinefrom Week 11, oruseCartStore) usingrenderHookfrom the testing library. - One deliberate snapshot test on a small presentational component, with a short comment explaining why a snapshot (rather than an explicit assertion) is the right tool there.
If a test involving TanStack Query hangs or times out, confirm you're wrapping the component under test in its own fresh QueryClientProvider per test — a shared client across tests carries cached state between them, which is a common source of tests that pass alone but fail in the full suite.
6. Knowledge Check
Four quick questions. Expand each to check your answer.
Q1
Why does jest-expo ship with built-in mocks for most Expo native modules?
Why does jest-expo ship with built-in mocks for most Expo native modules?
A Jest test runs in Node, with no real device, camera, or notification system available — without mocks, calling a native module in a test would throw or hang. jest-expo's mocks let code exercise those calls in a test the same way it would in the app, resolving to a fake but realistic result.
Q2
Why does React Native Testing Library favor queries like getByText or getByRole over selecting components by internal name?
Why does React Native Testing Library favor queries like getByText or getByRole over selecting components by internal name?
Those queries assert on what a real user actually perceives and interacts with — visible text, accessible roles and labels — so a test built on them only breaks when user-facing behavior genuinely changes, not when an internal refactor (renaming a component, restructuring its internals) leaves behavior identical.
Q3
Why is a fixed setTimeout before asserting on async UI worse than waitFor?
Why is a fixed setTimeout before asserting on async UI worse than waitFor?
A fixed delay is either too short (flaky, fails under slower CI load) or wastefully long (slows the whole suite down) — there's no value that's reliably correct. waitFor polls its assertion until it passes or a timeout is hit, adapting to how long the async work actually takes.
Q4
Why is a full-screen snapshot test a poor fit for a screen that changes frequently for legitimate reasons?
Why is a full-screen snapshot test a poor fit for a screen that changes frequently for legitimate reasons?
Every legitimate change produces a large, hard-to-review diff, which trains developers to run --updateSnapshot reflexively without actually reading what changed — at that point the test no longer catches real regressions, it just adds friction. An explicit assertion on the specific thing that matters (a particular piece of text, an element being present) stays meaningful through unrelated changes.