@testing-library/react-native 14.0.0-beta.1 → 14.0.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,83 @@
1
+ # Custom `render` function
2
+
3
+ ### Summary
4
+
5
+ RNTL exposes the `render` function as the primary entry point for tests. If you make complex, repeating setups for your tests, consider creating a custom render function. The idea is to encapsulate common setup steps and test wiring inside a render function suitable for your tests.
6
+
7
+ ### Example
8
+
9
+ ```tsx title=test-utils.ts
10
+ // ...
11
+
12
+ interface RenderWithProvidersProps {
13
+ user?: User | null;
14
+ theme?: Theme;
15
+ }
16
+
17
+ export async function renderWithProviders<T>(
18
+ ui: React.ReactElement<T>,
19
+ options?: RenderWithProvidersProps,
20
+ ) {
21
+ return await render(
22
+ <UserProvider.Provider value={options?.user ?? null}>
23
+ <ThemeProvider.Provider value={options?.theme ?? 'light'}>{ui}</ThemeProvider.Provider>
24
+ </UserProvider.Provider>,
25
+ );
26
+ }
27
+ ```
28
+
29
+ ```tsx title=custom-render/index.test.tsx
30
+ import { screen } from '@testing-library/react-native';
31
+ import { renderWithProviders } from '../test-utils';
32
+ // ...
33
+
34
+ test('renders WelcomeScreen with user', async () => {
35
+ await renderWithProviders(<WelcomeScreen />, { user: { name: 'Jar-Jar' } });
36
+ expect(screen.getByText(/hello Jar-Jar/i)).toBeOnTheScreen();
37
+ });
38
+
39
+ test('renders WelcomeScreen without user', async () => {
40
+ await renderWithProviders(<WelcomeScreen />, { user: null });
41
+ expect(screen.getByText(/hello stranger/i)).toBeOnTheScreen();
42
+ });
43
+ ```
44
+
45
+ Example [full source code](https://github.com/callstack/react-native-testing-library/tree/main/examples/cookbook/custom-render).
46
+
47
+ ### More info
48
+
49
+ #### Additional params
50
+
51
+ A custom render function might accept additional parameters to allow for setting up different start conditions for a test, e.g., the initial state for global state management.
52
+
53
+ ```tsx title=SomeScreen.test.tsx
54
+ test('renders SomeScreen for logged in user', async () => {
55
+ await renderScreen(<SomeScreen />, { state: loggedInState });
56
+ // ...
57
+ });
58
+ ```
59
+
60
+ #### Multiple functions
61
+
62
+ Depending on the situation, you may declare more than one custom render function. For example, you have one function for testing application flows and a second for testing individual screens.
63
+
64
+ ```tsx title=test-utils.tsx
65
+ function renderNavigator(ui, options);
66
+ function renderScreen(ui, options);
67
+ ```
68
+
69
+ #### Async setup
70
+
71
+ Since `render` is async, your custom render function should be marked as `async` and use `await render()`. This pattern also makes it easy to add additional async setup if needed:
72
+
73
+ ```tsx title=SomeScreen.test.tsx
74
+ async function renderWithData<T>(ui: React.ReactElement<T>) {
75
+ const data = await fetchTestData();
76
+ return await render(<DataProvider value={data}>{ui}</DataProvider>);
77
+ }
78
+
79
+ test('renders SomeScreen', async () => {
80
+ await renderWithData(<SomeScreen />);
81
+ // ...
82
+ });
83
+ ```
@@ -0,0 +1,375 @@
1
+ # Network Requests
2
+
3
+ ## Introduction
4
+
5
+ Mocking network requests is an essential part of testing React Native applications. By mocking
6
+ network
7
+ requests, you can control the data that is returned from the server and test how your application
8
+ behaves in different scenarios, such as when the request is successful or when it fails.
9
+
10
+ In this guide, we will show you how to mock network requests and guard your test suits from unwanted
11
+ and unmocked/unhandled network requests
12
+
13
+ > [!INFO]
14
+ > To simulate a real-world scenario, we will use the [Random User Generator API](https://randomuser.me/) that provides random user data.
15
+
16
+ ## Phonebook Example
17
+
18
+ Let's assume we have a simple phonebook application that
19
+ uses [`fetch`](https://reactnative.dev/docs/network#using-fetch) for fetching Data from a server.
20
+ In our case, we have a list of contacts and favorites that we want to display in our application.
21
+
22
+ This is how the root of the application looks like:
23
+
24
+ ```tsx title=network-requests/Phonebook.tsx
25
+ import React, { useEffect, useState } from 'react';
26
+ import { Text } from 'react-native';
27
+ import { User } from './types';
28
+ import ContactsList from './components/ContactsList';
29
+ import FavoritesList from './components/FavoritesList';
30
+ import getAllContacts from './api/getAllContacts';
31
+ import getAllFavorites from './api/getAllFavorites';
32
+
33
+ export default () => {
34
+ const [usersData, setUsersData] = useState<User[]>([]);
35
+ const [favoritesData, setFavoritesData] = useState<User[]>([]);
36
+ const [error, setError] = useState<string | null>(null);
37
+
38
+ useEffect(() => {
39
+ const _getAllContacts = async () => {
40
+ const _data = await getAllContacts();
41
+ setUsersData(_data);
42
+ };
43
+ const _getAllFavorites = async () => {
44
+ const _data = await getAllFavorites();
45
+ setFavoritesData(_data);
46
+ };
47
+
48
+ const run = async () => {
49
+ try {
50
+ await Promise.all([_getAllContacts(), _getAllFavorites()]);
51
+ } catch (e) {
52
+ const message = isErrorWithMessage(e) ? e.message : 'Something went wrong';
53
+ setError(message);
54
+ }
55
+ };
56
+
57
+ void run();
58
+ }, []);
59
+
60
+ if (error) {
61
+ return <Text>An error occurred: {error}</Text>;
62
+ }
63
+
64
+ return (
65
+ <>
66
+ <FavoritesList users={favoritesData} />
67
+ <ContactsList users={usersData} />
68
+ </>
69
+ );
70
+ };
71
+ ```
72
+
73
+ We fetch the contacts from the server using the `getAllFavorites` function that utilizes `fetch`.
74
+
75
+ ```tsx title=network-requests/api/getAllContacts.ts
76
+ import { User } from '../types';
77
+
78
+ export default async (): Promise<User[]> => {
79
+ const res = await fetch('https://randomuser.me/api/?results=25');
80
+ if (!res.ok) {
81
+ throw new Error(`Error fetching contacts`);
82
+ }
83
+ const json = await res.json();
84
+ return json.results;
85
+ };
86
+ ```
87
+
88
+ We have similar function for fetching the favorites, but this time limiting the results to 10.
89
+
90
+ ```tsx title=network-requests/api/getAllFavorites.ts
91
+ import { User } from '../types';
92
+
93
+ export default async (): Promise<User[]> => {
94
+ const res = await fetch('https://randomuser.me/api/?results=10');
95
+ if (!res.ok) {
96
+ throw new Error(`Error fetching favorites`);
97
+ }
98
+ const json = await res.json();
99
+ return json.results;
100
+ };
101
+ ```
102
+
103
+ Our `FavoritesList` component is a simple component that displays the list of favorite contacts and
104
+ their avatars horizontally.
105
+
106
+ ```tsx title=network-requests/components/FavoritesList.tsx
107
+ import {FlatList, Image, StyleSheet, Text, View} from 'react-native';
108
+ import React, {useCallback} from 'react';
109
+ import type {ListRenderItem} from '@react-native/virtualized-lists';
110
+ import {User} from '../types';
111
+
112
+ export default ({users}: { users: User[] }) => {
113
+ const renderItem: ListRenderItem<User> = useCallback(({item: {picture}}) => {
114
+ return (
115
+ <View style={styles.userContainer}>
116
+ <Image
117
+ source={{uri: picture.thumbnail}}
118
+ style={styles.userImage}
119
+ accessibilityLabel={'favorite-contact-avatar'}
120
+ />
121
+ </View>
122
+ );
123
+ }, []);
124
+
125
+ if (users.length === 0) return (
126
+ <View style={styles.loaderContainer}>
127
+ <Text>Figuring out your favorites...</Text>
128
+ </View>
129
+ );
130
+
131
+ return (
132
+ <View style={styles.outerContainer}>
133
+ <Text>⭐My Favorites</Text>
134
+ <FlatList<User>
135
+ horizontal
136
+ showsHorizontalScrollIndicator={false}
137
+ data={users}
138
+ renderItem={renderItem}
139
+ keyExtractor={(item, index) => `${index}-${item.id.value}`}
140
+ />
141
+ </View>
142
+ );
143
+ };
144
+
145
+ // Looking for styles?
146
+ // Check examples/cookbook/app/advanced/components/FavoritesList.tsx
147
+ const styles =
148
+ ...
149
+ ```
150
+
151
+ Our `ContactsList` component is similar to the `FavoritesList` component, but it displays the list
152
+ of
153
+ all contacts vertically.
154
+
155
+ ```tsx title=network-requests/components/ContactsList.tsx
156
+ import { FlatList, Image, StyleSheet, Text, View } from 'react-native';
157
+ import React, { useCallback } from 'react';
158
+ import type { ListRenderItem } from '@react-native/virtualized-lists';
159
+ import { User } from '../types';
160
+
161
+ export default ({ users }: { users: User[] }) => {
162
+ const renderItem: ListRenderItem<User> = useCallback(
163
+ ({ item: { name, email, picture, cell }, index }) => {
164
+ const { title, first, last } = name;
165
+ const backgroundColor = index % 2 === 0 ? '#f9f9f9' : '#fff';
166
+ return (
167
+ <View style={[{ backgroundColor }, styles.userContainer]}>
168
+ <Image source={{ uri: picture.thumbnail }} style={styles.userImage} />
169
+ <View>
170
+ <Text>
171
+ Name: {title} {first} {last}
172
+ </Text>
173
+ <Text>Email: {email}</Text>
174
+ <Text>Mobile: {cell}</Text>
175
+ </View>
176
+ </View>
177
+ );
178
+ },
179
+ [],
180
+ );
181
+
182
+ if (users.length === 0) return <FullScreenLoader />;
183
+
184
+ return (
185
+ <View>
186
+ <FlatList<User>
187
+ data={users}
188
+ renderItem={renderItem}
189
+ keyExtractor={(item, index) => `${index}-${item.id.value}`}
190
+ />
191
+ </View>
192
+ );
193
+ };
194
+
195
+ // Looking for styles or FullScreenLoader component?
196
+ // Check examples/cookbook/app/advanced/components/ContactsList.tsx
197
+ const FullScreenLoader = () => ...
198
+ const styles = ...
199
+ ```
200
+
201
+ ## Start testing with a simple test
202
+
203
+ In our initial test we would like to test if the `PhoneBook` component renders the `FavoritesList`
204
+ and `ContactsList` components correctly.
205
+ We will need to mock the network requests and their corresponding responses to ensure that the component behaves as
206
+ expected. To mock the network requests we will use [MSW (Mock Service Worker)](https://mswjs.io/docs/getting-started).
207
+
208
+ > [!NOTE]
209
+ > We recommend using the Mock Service Worker (MSW) library to declaratively mock API communication in your tests instead of stubbing `fetch`, or relying on third-party adapters.
210
+
211
+ > [!INFO]
212
+ > You can install MSW by running `npm install msw --save-dev` or `yarn add msw --dev`.
213
+ > More info regarding installation can be found in [MSW's getting started guide](https://mswjs.io/docs/getting-started#step-1-install).
214
+ >
215
+ > Please make sure you're also aware of [MSW's setup guide](https://mswjs.io/docs/integrations/react-native).
216
+ > Please be minded that the MSW's setup guide is potentially incomplete and might contain discrepancies/missing pieces.
217
+
218
+ ```tsx title=network-requests/Phonebook.test.tsx
219
+ import { render, screen, waitForElementToBeRemoved } from '@testing-library/react-native';
220
+ import React from 'react';
221
+ import PhoneBook from '../PhoneBook';
222
+ import { User } from '../types';
223
+ import {http, HttpResponse} from "msw";
224
+ import {setupServer} from "msw/node";
225
+
226
+ // Define request handlers and response resolvers for random user API.
227
+ // By default, we always return the happy path response.
228
+ const handlers = [
229
+ http.get('https://randomuser.me/api/*', () => {
230
+ return HttpResponse.json(DATA);
231
+ }),
232
+ ];
233
+
234
+ // Setup a request interception server with the given request handlers.
235
+ const server = setupServer(...handlers);
236
+
237
+ // Enable API mocking via Mock Service Worker (MSW)
238
+ beforeAll(() => server.listen());
239
+ // Reset any runtime request handlers we may add during the tests
240
+ afterEach(() => server.resetHandlers());
241
+ // Disable API mocking after the tests are done
242
+ afterAll(() => server.close());
243
+
244
+ describe('PhoneBook', () => {
245
+ it('fetches all contacts and favorites successfully and renders lists in sections correctly', async () => {
246
+ await render(<PhoneBook />);
247
+
248
+ await waitForElementToBeRemoved(() => screen.getByText(/users data not quite there yet/i));
249
+ expect(await screen.findByText('Name: Mrs Ida Kristensen')).toBeOnTheScreen();
250
+ expect(await screen.findByText('Email: ida.kristensen@example.com')).toBeOnTheScreen();
251
+ expect(await screen.findAllByText(/name/i)).toHaveLength(3);
252
+ expect(await screen.findByText(/my favorites/i)).toBeOnTheScreen();
253
+ expect(await screen.findAllByLabelText('favorite-contact-avatar')).toHaveLength(3);
254
+ });
255
+ });
256
+
257
+ const DATA: { results: User[] } = {
258
+ results: [
259
+ {
260
+ name: {
261
+ title: 'Mrs',
262
+ first: 'Ida',
263
+ last: 'Kristensen',
264
+ },
265
+ email: 'ida.kristensen@example.com',
266
+ id: {
267
+ name: 'CPR',
268
+ value: '250562-5730',
269
+ },
270
+ picture: {
271
+ large: 'https://randomuser.me/api/portraits/women/26.jpg',
272
+ medium: 'https://randomuser.me/api/portraits/med/women/26.jpg',
273
+ thumbnail: 'https://randomuser.me/api/portraits/thumb/women/26.jpg',
274
+ },
275
+ cell: '123-4567-890',
276
+ },
277
+ // For brevity, we have omitted the rest of the users, you can still find them in
278
+ // examples/cookbook/app/network-requests/__tests__/test-utils.ts
279
+ ...
280
+ ],
281
+ };
282
+ ```
283
+
284
+ > [!INFO]
285
+ > More info regarding how to describe the network using request handlers, intercepting a request and handling its response can be found in the [MSW's documentation](https://mswjs.io/docs/getting-started#step-2-describe).
286
+
287
+ ## Testing error handling
288
+
289
+ As we are dealing with network requests, and things can go wrong, we should also cover the case when
290
+ the API request fails. In this case, we would like to test how our application behaves when the API request fails.
291
+
292
+ > [!INFO]
293
+ > The nature of the network can be highly dynamic, which makes it challenging to describe it completely in a fixed list of request handlers.
294
+ > MSW provides us the means to override any particular network behavior using the designated `.use()` API.
295
+ > More info can be found in [MSW's Network behavior overrides documentation](https://mswjs.io/docs/best-practices/network-behavior-overrides)
296
+
297
+ ```tsx title=network-requests/Phonebook.test.tsx
298
+ ...
299
+
300
+ const mockServerFailureForGetAllContacts = () => {
301
+ server.use(
302
+ http.get('https://randomuser.me/api/', ({ request }) => {
303
+ // Construct a URL instance out of the intercepted request.
304
+ const url = new URL(request.url);
305
+ // Read the "results" URL query parameter using the "URLSearchParams" API.
306
+ const resultsLength = url.searchParams.get('results');
307
+ // Simulate a server error for the get all contacts request.
308
+ // We check if the "results" query parameter is set to "25"
309
+ // to know it's the correct request to mock, in our case get all contacts.
310
+ if (resultsLength === '25') {
311
+ return new HttpResponse(null, { status: 500 });
312
+ }
313
+ // Return the default response for all other requests that match URL and verb. (in our case get favorites)
314
+ return HttpResponse.json(DATA);
315
+ }),
316
+ );
317
+ };
318
+
319
+ describe('PhoneBook', () => {
320
+ ...
321
+ it('fails to fetch all contacts and renders error message', async () => {
322
+ mockServerFailureForGetAllContacts();
323
+ await render(<PhoneBook />);
324
+
325
+ await waitForElementToBeRemoved(() => screen.getByText(/users data not quite there yet/i));
326
+ expect(
327
+ await screen.findByText(/an error occurred: error fetching contacts/i),
328
+ ).toBeOnTheScreen();
329
+ });
330
+ });
331
+
332
+ ```
333
+
334
+ ## Global guarding against unwanted API requests
335
+
336
+ As mistakes may happen, we might forget to mock a network request in one of our tests in the future.
337
+ To prevent us from happening, and alert when a certain network request is left unhandled, you may choose to
338
+ move MSW's server management from `PhoneBook.test.tsx` to Jest's setup file via [`setupFilesAfterEnv`](https://jestjs.io/docs/configuration#setupfilesafterenv-array).
339
+
340
+ ```tsx title=examples/cookbook/jest-setup.ts
341
+ // Enable API mocking via Mock Service Worker (MSW)
342
+ beforeAll(() => server.listen());
343
+ // Reset any runtime request handlers we may add during the tests
344
+ afterEach(() => server.resetHandlers());
345
+ // Disable API mocking after the tests are done
346
+ afterAll(() => server.close());
347
+
348
+ // ... rest of your setup file
349
+ ```
350
+
351
+ This setup will ensure you have the MSW server running before any test suite starts and stops it after all tests are done.
352
+ Which will result in a warning in the console if you forget to mock an API request in your test suite.
353
+
354
+ ```bash
355
+ [MSW] Warning: intercepted a request without a matching request handler:
356
+ • GET https://randomuser.me/api/?results=25?results=25
357
+ ```
358
+
359
+ ## Conclusion
360
+
361
+ Testing a component that makes network requests in combination with MSW takes some initial preparation to configure and describe the overridden networks.
362
+ We can achieve that by using MSW's request handlers and intercepting APIs.
363
+
364
+ Once up and running we gain full grip over the network requests, their responses, statuses.
365
+ Doing so is crucial to be able to test how our application behaves in different
366
+ scenarios, such as when the request is successful or when it fails.
367
+
368
+ When global configuration is in place, MSW's will also warn us when an unhandled network requests has occurred throughout a test suite.
369
+
370
+ ## Further Reading and Alternatives
371
+
372
+ Explore more advanced scenarios for mocking network requests with MSW:
373
+
374
+ - MSW's Basics - [Intercepting requests](https://mswjs.io/docs/basics/intercepting-requests) and/or [Mocking responses](https://mswjs.io/docs/basics/mocking-responses)
375
+ - MSW's Network behavior - how to describe [REST](https://mswjs.io/docs/network-behavior/rest) and/or [GraphQL](https://mswjs.io/docs/network-behavior/graphql) APIs