Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 7fc4ebc

Browse files
kligarskiAnCichockasatya164
authoredMar 15, 2025··
Update testing guide (#1404)
Co-authored-by: AnCichocka <annacichocka.poczta@gmail.com> Co-authored-by: Satyajit Sahoo <satyajit.happy@gmail.com>
1 parent 1aaef1a commit 7fc4ebc

File tree

1 file changed

+692
-44
lines changed

1 file changed

+692
-44
lines changed
 

‎versioned_docs/version-7.x/testing.md

Lines changed: 692 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,115 +1,763 @@
11
---
22
id: testing
3-
title: Testing with Jest
4-
sidebar_label: Testing with Jest
3+
title: Writing tests
4+
sidebar_label: Writing tests
55
---
66

77
import Tabs from '@theme/Tabs';
88
import TabItem from '@theme/TabItem';
99

10-
Testing code using React Navigation may require some setup since we need to mock native dependencies used in the navigators. We recommend using [Jest](https://jestjs.io) to write unit tests.
10+
React Navigation components can be tested in a similar way to other React components. This guide will cover how to write tests for components using React Navigation using [Jest](https://jestjs.io).
1111

12-
## Mocking native modules
12+
## Guiding principles
13+
14+
When writing tests, it's encouraged to write tests that closely resemble how users interact with your app. Keeping this in mind, here are some guiding principles to follow:
15+
16+
- **Test the result, not the action**: Instead of checking if a specific navigation action was called, check if the expected components are rendered after navigation.
17+
- **Avoid mocking React Navigation**: Mocking React Navigation components can lead to tests that don't match the actual logic. Instead, use a real navigator in your tests.
18+
19+
Following these principles will help you write tests that are more reliable and easier to maintain by avoiding testing implementation details.
20+
21+
## Mocking native dependencies
1322

1423
To be able to test React Navigation components, certain dependencies will need to be mocked depending on which components are being used.
1524

16-
If you're using `@react-navigation/drawer`, you will need to mock:
25+
If you're using `@react-navigation/stack`, you will need to mock:
1726

18-
- `react-native-reanimated`
1927
- `react-native-gesture-handler`
2028

21-
If you're using `@react-navigation/stack`, you will only need to mock:
29+
If you're using `@react-navigation/drawer`, you will need to mock:
2230

31+
- `react-native-reanimated`
2332
- `react-native-gesture-handler`
2433

2534
To add the mocks, create a file `jest/setup.js` (or any other file name of your choice) and paste the following code in it:
2635

2736
```js
28-
// include this line for mocking react-native-gesture-handler
37+
// Include this line for mocking react-native-gesture-handler
2938
import 'react-native-gesture-handler/jestSetup';
3039

31-
// include this section and the NativeAnimatedHelper section for mocking react-native-reanimated
32-
jest.mock('react-native-reanimated', () => {
33-
const Reanimated = require('react-native-reanimated/mock');
40+
// Include this section for mocking react-native-reanimated
41+
import { setUpTests } from 'react-native-reanimated';
3442

35-
// The mock for `call` immediately calls the callback which is incorrect
36-
// So we override it with a no-op
37-
Reanimated.default.call = () => {};
38-
39-
return Reanimated;
40-
});
43+
setUpTests();
4144

4245
// Silence the warning: Animated: `useNativeDriver` is not supported because the native animated module is missing
46+
import { jest } from '@jest/globals';
47+
4348
jest.mock('react-native/Libraries/Animated/NativeAnimatedHelper');
4449
```
4550

46-
Then we need to use this setup file in our jest config. You can add it under `setupFiles` option in a `jest.config.js` file or the `jest` key in `package.json`:
51+
Then we need to use this setup file in our jest config. You can add it under `setupFilesAfterEnv` option in a `jest.config.js` file or the `jest` key in `package.json`:
4752

4853
```json
4954
{
5055
"preset": "react-native",
51-
"setupFiles": ["<rootDir>/jest/setup.js"]
56+
"setupFilesAfterEnv": ["<rootDir>/jest/setup.js"]
5257
}
5358
```
5459

55-
Make sure that the path to the file in `setupFiles` is correct. Jest will run these files before running your tests, so it's the best place to put your global mocks.
60+
Make sure that the path to the file in `setupFilesAfterEnv` is correct. Jest will run these files before running your tests, so it's the best place to put your global mocks.
61+
62+
<details>
63+
<summary>Mocking `react-native-screens`</summary>
64+
65+
This shouldn't be necessary in most cases. However, if you find yourself in a need to mock `react-native-screens` component for some reason, you should do it by adding following code in `jest/setup.js` file:
66+
67+
```js
68+
// Include this section for mocking react-native-screens
69+
jest.mock('react-native-screens', () => {
70+
// Require actual module instead of a mock
71+
let screens = jest.requireActual('react-native-screens');
72+
73+
// All exports in react-native-screens are getters
74+
// We cannot use spread for cloning as it will call the getters
75+
// So we need to clone it with Object.create
76+
screens = Object.create(
77+
Object.getPrototypeOf(screens),
78+
Object.getOwnPropertyDescriptors(screens)
79+
);
80+
81+
// Add mock of the component you need
82+
// Here is the example of mocking the Screen component as a View
83+
Object.defineProperty(screens, 'Screen', {
84+
value: require('react-native').View,
85+
});
86+
87+
return screens;
88+
});
89+
```
90+
91+
</details>
5692

5793
If you're not using Jest, then you'll need to mock these modules according to the test framework you are using.
5894

59-
## Writing tests
95+
## Fake timers
96+
97+
When writing tests containing navigation with animations, you need to wait until the animations finish. In such cases, we recommend using [`Fake Timers`](https://jestjs.io/docs/timer-mocks) to simulate the passage of time in your tests. This can be done by adding the following line at the beginning of your test file:
98+
99+
```js
100+
jest.useFakeTimers();
101+
```
102+
103+
Fake timers replace real implementation of the native timer functions (e.g. `setTimeout()`, `setInterval()` etc,) with a custom implementation that uses a fake clock. This lets you instantly skip animations and reduce the time needed to run your tests by calling methods such as `jest.runAllTimers()`.
104+
105+
Often, component state is updated after an animation completes. To avoid getting an error in such cases, wrap `jest.runAllTimers()` in `act`:
106+
107+
```js
108+
import { act } from 'react-test-renderer';
109+
110+
// ...
111+
112+
act(() => jest.runAllTimers());
113+
```
114+
115+
See the examples below for more details on how to use fake timers in tests involving navigation.
60116

61-
We recommend using [React Native Testing Library](https://callstack.github.io/react-native-testing-library/) along with [`jest-native`](https://github.com/testing-library/jest-native) to write your tests.
117+
## Navigation and visibility
118+
119+
In React Navigation, the previous screen is not unmounted when navigating to a new screen. This means that the previous screen is still present in the component tree, but it's not visible.
120+
121+
When writing tests, you should assert that the expected component is visible or hidden instead of checking if it's rendered or not. React Native Testing Library provides a `toBeVisible` matcher that can be used to check if an element is visible to the user.
122+
123+
```js
124+
expect(screen.getByText('Settings screen')).toBeVisible();
125+
```
126+
127+
This is in contrast to the `toBeOnTheScreen` matcher, which checks if the element is rendered in the component tree. This matcher is not recommended when writing tests involving navigation.
128+
129+
By default, the queries from React Native Testing Library (e.g. `getByRole`, `getByText`, `getByLabelText` etc.) [only return visible elements](https://callstack.github.io/react-native-testing-library/docs/api/queries#includehiddenelements-option). So you don't need to do anything special. However, if you're using a different library for your tests, you'll need to account for this behavior.
130+
131+
## Example tests
132+
133+
We recommend using [React Native Testing Library](https://callstack.github.io/react-native-testing-library/) to write your tests.
134+
135+
In this guide, we will go through some example scenarios and show you how to write tests for them using Jest and React Native Testing Library:
136+
137+
### Navigation between tabs
138+
139+
In this example, we have a bottom tab navigator with two tabs: Home and Settings. We will write a test that asserts that we can navigate between these tabs by pressing the tab bar buttons.
140+
141+
<Tabs groupId="example" queryString="example">
142+
<TabItem value="static" label="Static" default>
143+
144+
```js title="MyTabs.js"
145+
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
146+
import { Text, View } from 'react-native';
147+
148+
const HomeScreen = () => {
149+
return (
150+
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
151+
<Text>Home screen</Text>
152+
</View>
153+
);
154+
};
155+
156+
const SettingsScreen = () => {
157+
return (
158+
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
159+
<Text>Settings screen</Text>
160+
</View>
161+
);
162+
};
163+
164+
export const MyTabs = createBottomTabNavigator({
165+
screens: {
166+
Home: HomeScreen,
167+
Settings: SettingsScreen,
168+
},
169+
});
170+
```
171+
172+
</TabItem>
173+
<TabItem value="dynamic" label="Dynamic">
174+
175+
```js title="MyTabs.js"
176+
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
177+
import { Text, View } from 'react-native';
178+
179+
const HomeScreen = () => {
180+
return (
181+
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
182+
<Text>Home screen</Text>
183+
</View>
184+
);
185+
};
186+
187+
const SettingsScreen = () => {
188+
return (
189+
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
190+
<Text>Settings screen</Text>
191+
</View>
192+
);
193+
};
194+
195+
const Tab = createBottomTabNavigator();
196+
197+
export const MyTabs = () => {
198+
return (
199+
<Tab.Navigator>
200+
<Tab.Screen name="Home" component={HomeScreen} />
201+
<Tab.Screen name="Settings" component={SettingsScreen} />
202+
</Tab.Navigator>
203+
);
204+
};
205+
```
62206

63-
Example:
207+
</TabItem>
208+
</Tabs>
64209

65-
<Tabs groupId="config" queryString="config">
210+
<Tabs groupId="example" queryString="example">
66211
<TabItem value="static" label="Static" default>
67212

68-
```js name='Testing with jest'
69-
import * as React from 'react';
70-
import { screen, render, fireEvent } from '@testing-library/react-native';
213+
```js title="MyTabs.test.js"
214+
import { expect, jest, test } from '@jest/globals';
71215
import { createStaticNavigation } from '@react-navigation/native';
72-
import { RootNavigator } from './RootNavigator';
216+
import { act, render, screen, userEvent } from '@testing-library/react-native';
217+
218+
import { MyTabs } from './MyTabs';
219+
220+
jest.useFakeTimers();
73221

74-
const Navigation = createStaticNavigation(RootNavigator);
222+
test('navigates to settings by tab bar button press', async () => {
223+
const user = userEvent.setup();
224+
225+
const Navigation = createStaticNavigation(MyTabs);
75226

76-
test('shows profile screen when View Profile is pressed', () => {
77227
render(<Navigation />);
78228

79-
fireEvent.press(screen.getByText('View Profile'));
229+
const button = screen.getByRole('button', { name: 'Settings, tab, 2 of 2' });
230+
231+
await user.press(button);
80232

81-
expect(screen.getByText('My Profile')).toBeOnTheScreen();
233+
act(() => jest.runAllTimers());
234+
235+
expect(screen.getByText('Settings screen')).toBeVisible();
82236
});
83237
```
84238

85239
</TabItem>
86240
<TabItem value="dynamic" label="Dynamic">
87241

88-
```js name='Testing with jest'
89-
import * as React from 'react';
90-
import { screen, render, fireEvent } from '@testing-library/react-native';
242+
```js title="MyTabs.test.js"
243+
import { expect, jest, test } from '@jest/globals';
91244
import { NavigationContainer } from '@react-navigation/native';
92-
import { RootNavigator } from './RootNavigator';
245+
import { act, render, screen, userEvent } from '@testing-library/react-native';
246+
247+
import { MyTabs } from './MyTabs';
248+
249+
jest.useFakeTimers();
250+
251+
test('navigates to settings by tab bar button press', async () => {
252+
const user = userEvent.setup();
93253

94-
test('shows profile screen when View Profile is pressed', () => {
95254
render(
96255
<NavigationContainer>
97-
<RootNavigator />
256+
<MyTabs />
98257
</NavigationContainer>
99258
);
100259

101-
fireEvent.press(screen.getByText('View Profile'));
260+
const button = screen.getByLabelText('Settings, tab, 2 of 2');
261+
262+
await user.press(button);
263+
264+
act(() => jest.runAllTimers());
102265

103-
expect(screen.getByText('My Profile')).toBeOnTheScreen();
266+
expect(screen.getByText('Settings screen')).toBeVisible();
104267
});
105268
```
106269

107270
</TabItem>
108271
</Tabs>
109272

110-
## Best practices
273+
In the above test, we:
274+
275+
- Render the `MyTabs` navigator within a [NavigationContainer](navigation-container.md) in our test.
276+
- Get the tab bar button using the `getByLabelText` query that matches its accessibility label.
277+
- Press the button using `userEvent.press(button)` to simulate a user interaction.
278+
- Run all timers using `jest.runAllTimers()` to skip animations (e.g. animations in the `Pressable` for the button).
279+
- Assert that the `Settings screen` is visible after the navigation.
280+
281+
### Reacting to a navigation event
282+
283+
In this example, we have a stack navigator with two screens: Home and Surprise. We will write a test that asserts that the text "Surprise!" is displayed after navigating to the Surprise screen.
284+
285+
<Tabs groupId="example" queryString="example">
286+
<TabItem value="static" label="Static" default>
287+
288+
```js title="MyStack.js"
289+
import { useNavigation } from '@react-navigation/native';
290+
import { createStackNavigator } from '@react-navigation/stack';
291+
import { Button, Text, View } from 'react-native';
292+
import { useEffect, useState } from 'react';
293+
294+
const HomeScreen = () => {
295+
const navigation = useNavigation();
296+
297+
return (
298+
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
299+
<Text>Home screen</Text>
300+
<Button
301+
onPress={() => navigation.navigate('Surprise')}
302+
title="Click here!"
303+
/>
304+
</View>
305+
);
306+
};
307+
308+
const SurpriseScreen = () => {
309+
const navigation = useNavigation();
310+
311+
const [textVisible, setTextVisible] = useState(false);
312+
313+
useEffect(() => {
314+
navigation.addListener('transitionEnd', () => setTextVisible(true));
315+
}, [navigation]);
316+
317+
return (
318+
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
319+
{textVisible ? <Text>Surprise!</Text> : ''}
320+
</View>
321+
);
322+
};
323+
324+
export const MyStack = createStackNavigator({
325+
screens: {
326+
Home: HomeScreen,
327+
Surprise: SurpriseScreen,
328+
},
329+
});
330+
```
331+
332+
</TabItem>
333+
<TabItem value="dynamic" label="Dynamic">
334+
335+
```js title="MyStack.js"
336+
import { useNavigation } from '@react-navigation/native';
337+
import { createStackNavigator } from '@react-navigation/stack';
338+
import { useEffect, useState } from 'react';
339+
import { Button, Text, View } from 'react-native';
340+
341+
const HomeScreen = () => {
342+
const navigation = useNavigation();
343+
344+
return (
345+
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
346+
<Text>Home screen</Text>
347+
<Button
348+
onPress={() => navigation.navigate('Surprise')}
349+
title="Click here!"
350+
/>
351+
</View>
352+
);
353+
};
354+
355+
const SurpriseScreen = () => {
356+
const navigation = useNavigation();
357+
358+
const [textVisible, setTextVisible] = useState(false);
359+
360+
useEffect(() => {
361+
navigation.addListener('transitionEnd', () => setTextVisible(true));
362+
}, [navigation]);
363+
364+
return (
365+
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
366+
{textVisible ? <Text>Surprise!</Text> : ''}
367+
</View>
368+
);
369+
};
370+
371+
const Stack = createStackNavigator();
372+
373+
export const MyStack = () => {
374+
return (
375+
<Stack.Navigator>
376+
<Stack.Screen name="Home" component={HomeScreen} />
377+
<Stack.Screen name="Surprise" component={SurpriseScreen} />
378+
</Stack.Navigator>
379+
);
380+
};
381+
```
382+
383+
</TabItem>
384+
</Tabs>
385+
386+
<Tabs groupId="example" queryString="example">
387+
<TabItem value="static" label="Static" default>
388+
389+
```js title="MyStack.test.js"
390+
import { expect, jest, test } from '@jest/globals';
391+
import { createStaticNavigation } from '@react-navigation/native';
392+
import { act, render, screen, userEvent } from '@testing-library/react-native';
393+
394+
import { MyStack } from './MyStack';
395+
396+
jest.useFakeTimers();
397+
398+
test('shows surprise text after navigating to surprise screen', async () => {
399+
const user = userEvent.setup();
400+
401+
const Navigation = createStaticNavigation(MyStack);
402+
403+
render(<Navigation />);
404+
405+
await user.press(screen.getByLabelText('Click here!'));
406+
407+
act(() => jest.runAllTimers());
408+
409+
expect(screen.getByText('Surprise!')).toBeVisible();
410+
});
411+
```
412+
413+
</TabItem>
414+
<TabItem value="dynamic" label="Dynamic">
415+
416+
```js title="MyStack.test.js"
417+
import { expect, jest, test } from '@jest/globals';
418+
import { NavigationContainer } from '@react-navigation/native';
419+
import { act, render, screen, userEvent } from '@testing-library/react-native';
420+
421+
import { MyStack } from './MyStack';
422+
423+
jest.useFakeTimers();
424+
425+
test('shows surprise text after navigating to surprise screen', async () => {
426+
const user = userEvent.setup();
427+
428+
render(
429+
<NavigationContainer>
430+
<MyStack />
431+
</NavigationContainer>
432+
);
433+
434+
await user.press(screen.getByLabelText('Click here!'));
435+
436+
act(() => jest.runAllTimers());
437+
438+
expect(screen.getByText('Surprise!')).toBeVisible();
439+
});
440+
```
441+
442+
</TabItem>
443+
</Tabs>
444+
445+
In the above test, we:
446+
447+
- Render the `MyStack` navigator within a [NavigationContainer](navigation-container.md) in our test.
448+
- Get the button using the `getByLabelText` query that matches its title.
449+
- Press the button using `userEvent.press(button)` to simulate a user interaction.
450+
- Run all timers using `jest.runAllTimers()` to skip animations (e.g. navigation animation between screens).
451+
- Assert that the `Surprise!` text is visible after the transition to the Surprise screen is complete.
452+
453+
### Fetching data `useFocusEffect`
454+
455+
In this example, we have a bottom tab navigator with two tabs: Home and Pokemon. We will write a test that asserts the data fetching logic on focus in the Pokemon screen.
456+
457+
<Tabs groupId="example" queryString="example">
458+
<TabItem value="static" label="Static" default>
459+
460+
```js title="MyTabs.js"
461+
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
462+
import { useFocusEffect } from '@react-navigation/native';
463+
import { useCallback, useState } from 'react';
464+
import { Text, View } from 'react-native';
465+
466+
function HomeScreen() {
467+
return (
468+
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
469+
<Text>Home screen</Text>
470+
</View>
471+
);
472+
}
473+
474+
const url = 'https://pokeapi.co/api/v2/pokemon/ditto';
475+
476+
function PokemonScreen() {
477+
const [profileData, setProfileData] = useState({ status: 'loading' });
478+
479+
useFocusEffect(
480+
useCallback(() => {
481+
if (profileData.status === 'success') {
482+
return;
483+
}
484+
485+
setProfileData({ status: 'loading' });
486+
487+
const controller = new AbortController();
488+
489+
const fetchUser = async () => {
490+
try {
491+
const response = await fetch(url, { signal: controller.signal });
492+
const data = await response.json();
493+
494+
setProfileData({ status: 'success', data: data });
495+
} catch (error) {
496+
setProfileData({ status: 'error' });
497+
}
498+
};
499+
500+
fetchUser();
501+
502+
return () => {
503+
controller.abort();
504+
};
505+
}, [profileData.status])
506+
);
507+
508+
if (profileData.status === 'loading') {
509+
return (
510+
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
511+
<Text>Loading...</Text>
512+
</View>
513+
);
514+
}
515+
516+
if (profileData.status === 'error') {
517+
return (
518+
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
519+
<Text>An error occurred!</Text>
520+
</View>
521+
);
522+
}
523+
524+
return (
525+
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
526+
<Text>{profileData.data.name}</Text>
527+
</View>
528+
);
529+
}
530+
531+
export const MyTabs = createBottomTabNavigator({
532+
screens: {
533+
Home: HomeScreen,
534+
Pokemon: PokemonScreen,
535+
},
536+
});
537+
```
538+
539+
</TabItem>
540+
<TabItem value="dynamic" label="Dynamic">
541+
542+
```js title="MyTabs.js"
543+
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
544+
import { useFocusEffect } from '@react-navigation/native';
545+
import { useCallback, useState } from 'react';
546+
import { Text, View } from 'react-native';
547+
548+
function HomeScreen() {
549+
return (
550+
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
551+
<Text>Home screen</Text>
552+
</View>
553+
);
554+
}
555+
556+
const url = 'https://pokeapi.co/api/v2/pokemon/ditto';
557+
558+
function PokemonInfoScreen() {
559+
const [profileData, setProfileData] = useState({ status: 'loading' });
560+
561+
useFocusEffect(
562+
useCallback(() => {
563+
if (profileData.status === 'success') {
564+
return;
565+
}
566+
567+
setProfileData({ status: 'loading' });
568+
569+
const controller = new AbortController();
570+
571+
const fetchUser = async () => {
572+
try {
573+
const response = await fetch(url, { signal: controller.signal });
574+
const data = await response.json();
575+
576+
setProfileData({ status: 'success', data: data });
577+
} catch (error) {
578+
setProfileData({ status: 'error' });
579+
}
580+
};
581+
582+
fetchUser();
583+
584+
return () => {
585+
controller.abort();
586+
};
587+
}, [profileData.status])
588+
);
589+
590+
if (profileData.status === 'loading') {
591+
return (
592+
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
593+
<Text>Loading...</Text>
594+
</View>
595+
);
596+
}
597+
598+
if (profileData.status === 'error') {
599+
return (
600+
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
601+
<Text>An error occurred!</Text>
602+
</View>
603+
);
604+
}
605+
606+
return (
607+
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
608+
<Text>{profileData.data.name}</Text>
609+
</View>
610+
);
611+
}
612+
613+
const Tab = createBottomTabNavigator();
614+
615+
export function MyTabs() {
616+
return (
617+
<Tab.Navigator screenOptions={{ headerShown: false }}>
618+
<Tab.Screen name="Home" component={HomeScreen} />
619+
<Tab.Screen name="Pokemon" component={PokemonScreen} />
620+
</Tab.Navigator>
621+
);
622+
}
623+
```
624+
625+
</TabItem>
626+
</Tabs>
627+
628+
To make the test deterministic and isolate it from the real backend, you can mock the network requests with a library such as [Mock Service Worker](https://mswjs.io/):
629+
630+
```js title="msw-handlers.js"
631+
import { delay, http, HttpResponse } from 'msw';
632+
633+
export const handlers = [
634+
http.get('https://pokeapi.co/api/v2/pokemon/ditto', async () => {
635+
await delay(1000);
636+
637+
return HttpResponse.json({
638+
id: 132,
639+
name: 'ditto',
640+
});
641+
}),
642+
];
643+
```
644+
645+
Here we setup a handler that mocks responses from the API (for this example we're using [PokéAPI](https://pokeapi.co/)). Additionally, we `delay` the response by 1000ms to simulate a network request delay.
646+
647+
Then, we write a Node.js integration module to use the Mock Service Worker in our tests:
648+
649+
```js title="msw-node.js"
650+
import { setupServer } from 'msw/node';
651+
import { handlers } from './msw-handlers';
652+
653+
const server = setupServer(...handlers);
654+
```
655+
656+
Refer to the documentation of the library to learn more about setting it up in your project - [Getting started](https://mswjs.io/docs/getting-started), [React Native integration](https://mswjs.io/docs/integrations/react-native).
657+
658+
<Tabs groupId="example" queryString="example">
659+
<TabItem value="static" label="Static" default>
660+
661+
```js title="MyTabs.test.js"
662+
import './msw-node';
663+
664+
import { expect, jest, test } from '@jest/globals';
665+
import { createStaticNavigation } from '@react-navigation/native';
666+
import { act, render, screen, userEvent } from '@testing-library/react-native';
667+
668+
import { MyTabs } from './MyTabs';
669+
670+
jest.useFakeTimers();
671+
672+
test('loads data on Pokemon info screen after focus', async () => {
673+
const user = userEvent.setup();
674+
675+
const Navigation = createStaticNavigation(MyTabs);
676+
677+
render(<Navigation />);
678+
679+
const homeTabButton = screen.getByLabelText('Home, tab, 1 of 2');
680+
const profileTabButton = screen.getByLabelText('Profile, tab, 2 of 2');
681+
682+
await user.press(profileTabButton);
683+
684+
expect(screen.getByText('Loading...')).toBeVisible();
685+
686+
await act(() => jest.runAllTimers());
687+
688+
expect(screen.getByText('ditto')).toBeVisible();
689+
690+
await user.press(homeTabButton);
691+
692+
await act(() => jest.runAllTimers());
693+
694+
await user.press(profileTabButton);
695+
696+
expect(screen.queryByText('Loading...')).not.toBeVisible();
697+
expect(screen.getByText('ditto')).toBeVisible();
698+
});
699+
```
700+
701+
</TabItem>
702+
<TabItem value="dynamic" label="Dynamic">
703+
704+
```js title="MyTabs.test.js"
705+
import './msw-node';
706+
707+
import { expect, jest, test } from '@jest/globals';
708+
import { NavigationContainer } from '@react-navigation/native';
709+
import { act, render, screen, userEvent } from '@testing-library/react-native';
710+
711+
import { MyTabs } from './MyTabs';
712+
713+
jest.useFakeTimers();
714+
715+
test('loads data on Pokemon info screen after focus', async () => {
716+
const user = userEvent.setup();
717+
718+
render(
719+
<NavigationContainer>
720+
<MyTabs />
721+
</NavigationContainer>
722+
);
723+
724+
const homeTabButton = screen.getByLabelText('Home, tab, 1 of 2');
725+
const profileTabButton = screen.getByLabelText('Profile, tab, 2 of 2');
726+
727+
await user.press(profileTabButton);
728+
729+
expect(screen.getByText('Loading...')).toBeVisible();
730+
731+
await act(() => jest.runAllTimers());
732+
733+
expect(screen.getByText('ditto')).toBeVisible();
734+
735+
await user.press(homeTabButton);
736+
737+
await act(() => jest.runAllTimers());
738+
739+
await user.press(profileTabButton);
740+
741+
expect(screen.queryByText('Loading...')).not.toBeVisible();
742+
expect(screen.getByText('ditto')).toBeVisible();
743+
});
744+
```
745+
746+
</TabItem>
747+
</Tabs>
748+
749+
In the above test, we:
750+
751+
- Assert that the `Loading...` text is visible while the data is being fetched.
752+
- Run all timers using `jest.runAllTimers()` to skip delays in the network request.
753+
- Assert that the `ditto` text is visible after the data is fetched.
754+
- Press the home tab button to navigate to the home screen.
755+
- Run all timers using `jest.runAllTimers()` to skip animations (e.g. animations in the `Pressable` for the button).
756+
- Press the profile tab button to navigate back to the Pokemon screen.
757+
- Ensure that cached data is shown by asserting that the `Loading...` text is not visible and the `ditto` text is visible.
758+
759+
:::note
111760

112-
There are a couple of things to keep in mind when writing tests for components using React Navigation:
761+
In a production app, we recommend using a library like [React Query](https://react-query.tanstack.com/) to handle data fetching and caching. The above example is for demonstration purposes only.
113762

114-
1. Avoid mocking React Navigation. Instead, use a real navigator in your tests.
115-
2. Don't check for navigation actions. Instead, check for the result of the navigation such as the screen being rendered.
763+
:::

0 commit comments

Comments
 (0)
Please sign in to comment.