Skip to content

Commit d105659

Browse files
committed
Compose Fetch recipe
1 parent cfdbe42 commit d105659

File tree

5 files changed

+268
-7
lines changed

5 files changed

+268
-7
lines changed

examples/cookbook/app/network-requests/__tests__/PhoneBook.test.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ jest.mock('axios');
99
describe('PhoneBook', () => {
1010
it('fetches contacts successfully and renders in list', async () => {
1111
(global.fetch as jest.SpyInstance).mockResolvedValueOnce({
12+
ok: true,
1213
json: jest.fn().mockResolvedValueOnce(DATA),
1314
});
1415
(axios.get as jest.Mock).mockResolvedValue({ data: DATA });
@@ -20,8 +21,20 @@ describe('PhoneBook', () => {
2021
expect(await screen.findAllByText(/name/i)).toHaveLength(3);
2122
});
2223

24+
it('fails to fetch contacts and renders error message', async () => {
25+
(global.fetch as jest.SpyInstance).mockResolvedValueOnce({
26+
ok: false,
27+
});
28+
(axios.get as jest.Mock).mockResolvedValue({ data: DATA });
29+
render(<PhoneBook />);
30+
31+
await waitForElementToBeRemoved(() => screen.getByText(/users data not quite there yet/i));
32+
expect(await screen.findByText(/error fetching contacts/i)).toBeOnTheScreen();
33+
});
34+
2335
it('fetches favorites successfully and renders all users avatars', async () => {
2436
(global.fetch as jest.SpyInstance).mockResolvedValueOnce({
37+
ok: true,
2538
json: jest.fn().mockResolvedValueOnce(DATA),
2639
});
2740
(axios.get as jest.Mock).mockResolvedValue({ data: DATA });
@@ -34,6 +47,7 @@ describe('PhoneBook', () => {
3447

3548
it('fails to fetch favorites and renders error message', async () => {
3649
(global.fetch as jest.SpyInstance).mockResolvedValueOnce({
50+
ok: true,
3751
json: jest.fn().mockResolvedValueOnce(DATA),
3852
});
3953
(axios.get as jest.Mock).mockRejectedValueOnce({ message: 'Error fetching favorites' });

examples/cookbook/app/network-requests/api/getAllContacts.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ import { User } from '../types';
22

33
export default async (): Promise<User[]> => {
44
const res = await fetch('https://randomuser.me/api/?results=25');
5+
if (!res.ok) {
6+
throw new Error(`Error fetching contacts`);
7+
}
58
const json = await res.json();
69
return json.results;
710
};

examples/cookbook/jest-setup.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,6 @@ beforeAll(() => {
1616
// see examples/cookbook/__mocks__/axios.ts
1717
});
1818
afterAll(() => {
19+
// restore the original fetch function
1920
(global.fetch as jest.Mock).mockRestore();
2021
});

website/docs/12.x/cookbook/network-requests/axios.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@ In this guide, we will show you how to mock Axios requests and guard your test s
88
and unmocked API requests.
99

1010
:::info
11-
To simulate a real-world scenario, we will use
12-
the [Random User Generator API](https://randomuser.me/) that provides random user data.
11+
To simulate a real-world scenario, we will use the [Random User Generator API](https://randomuser.me/) that provides random user data.
1312
:::
1413

1514
## Phonebook Example
@@ -19,7 +18,7 @@ In our case, we have a list of favorite contacts that we want to display in our
1918

2019
This is how the root of the application looks like:
2120

22-
```tsx title=network-requests/axios/Phonebook.tsx
21+
```tsx title=network-requests/Phonebook.tsx
2322
import React, {useEffect, useState} from 'react';
2423
import {User} from './types';
2524
import FavoritesList from './components/FavoritesList';
@@ -54,7 +53,7 @@ export default () => {
5453

5554
We fetch the contacts from the server using the `getAllFavorites` function that utilizes Axios.
5655

57-
```tsx title=network-requests/axios/api/getAllFavorites.ts
56+
```tsx title=network-requests/api/getAllFavorites.ts
5857
import axios from 'axios';
5958
import {User} from '../types';
6059

@@ -68,7 +67,7 @@ export default async (): Promise<User[]> => {
6867
Our `FavoritesList` component is a simple component that displays the list of favorite contacts and
6968
their avatars.
7069

71-
```tsx title=network-requests/axios/components/FavoritesList.tsx
70+
```tsx title=network-requests/components/FavoritesList.tsx
7271
import {FlatList, Image, StyleSheet, Text, View} from 'react-native';
7372
import React, {useCallback} from 'react';
7473
import type {ListRenderItem} from '@react-native/virtualized-lists';
@@ -116,10 +115,13 @@ const styles =
116115
## Start testing with a simple test
117116
In our test we will make sure we mock the `axios.get` function to return the data we want.
118117
In this specific case, we will return a list of 3 users.
118+
119+
::info
119120
By using `mockResolvedValueOnce` we gain more grip and prevent the mock from resolving the data
120121
multiple times, which might lead to unexpected behavior.
122+
:::
121123

122-
```tsx title=network-requests/axios/Phonebook.test.tsx
124+
```tsx title=network-requests/Phonebook.test.tsx
123125
import {render, waitForElementToBeRemoved} from '@testing-library/react-native';
124126
import React from 'react';
125127
import PhoneBook from '../PhoneBook';
@@ -179,7 +181,7 @@ handling the error correctly.
179181
It is good to note that Axios throws auto. an error when the response status code is not in the range of 2xx.
180182
:::
181183

182-
```tsx title=network-requests/axios/Phonebook.test.tsx
184+
```tsx title=network-requests/Phonebook.test.tsx
183185
...
184186
it('fails to fetch favorites and renders error message', async () => {
185187
// Mock the axios.get function to throw an error
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,242 @@
11
# Fetch
2+
3+
## Introduction
4+
5+
React Native provides the Fetch API for your networking needs. It is promise-based and provides a
6+
simple and clean API for making requests. In this guide, we will show you how to mock `fetch` requests
7+
and guard your test suits from unwanted and unmocked API requests.
8+
9+
:::info
10+
To simulate a real-world scenario, we will use
11+
the [Random User Generator API](https://randomuser.me/)
12+
that provides random user data.
13+
:::
14+
15+
## Phonebook Example
16+
17+
Let's assume we have a simple phonebook application that uses `fetch` for fetching Data from a server.
18+
In our case, we have a list of contacts that we want to display in our application.
19+
20+
This is how the root of the application looks like:
21+
22+
```tsx title=network-requests/Phonebook.tsx
23+
import React, {useEffect, useState} from 'react';
24+
import {User} from './types';
25+
import FavoritesList from './components/FavoritesList';
26+
27+
export default () => {
28+
const [usersData, setUsersData] = useState<User[]>([]);
29+
const [error, setError] = useState<string | null>(null);
30+
31+
useEffect(() => {
32+
const run = async () => {
33+
try {
34+
const _data = await getAllContacts();
35+
setUsersData(_data);
36+
} catch (e) {
37+
setError(e.message);
38+
}
39+
};
40+
41+
void run();
42+
}, []);
43+
44+
if (error) {
45+
return <Text>An error occurred: {error}</Text>;
46+
}
47+
48+
return (
49+
<ContactsList users={usersData}/>
50+
);
51+
};
52+
53+
```
54+
55+
We fetch the contacts from the server using the `getAllContacts` function that utilizes `fetch`.
56+
57+
```tsx title=network-requests/api/getAllContacts.ts
58+
import {User} from '../types';
59+
60+
export default async (): Promise<User[]> => {
61+
const res = await fetch('https://randomuser.me/api/?results=25');
62+
if (!res.ok) {
63+
throw new Error(`Error fetching contacts`);
64+
}
65+
const json = await res.json();
66+
return json.results;
67+
};
68+
```
69+
70+
Our `ContactsList` component is a simple component that displays the list of favorite contacts and
71+
their avatars.
72+
73+
```tsx title=network-requests/components/ContactsList.tsx
74+
import {FlatList, Image, StyleSheet, Text, View} from 'react-native';
75+
import React, {useCallback} from 'react';
76+
import type {ListRenderItem} from '@react-native/virtualized-lists';
77+
import {User} from '../types';
78+
79+
export default ({users}: { users: User[] }) => {
80+
const renderItem: ListRenderItem<User> = useCallback(
81+
({item: {name, email, picture, cell}, index}) => {
82+
const {title, first, last} = name;
83+
const backgroundColor = index % 2 === 0 ? '#f9f9f9' : '#fff';
84+
return (
85+
<View style={[{backgroundColor}, styles.userContainer]}>
86+
<Image source={{uri: picture.thumbnail}} style={styles.userImage}/>
87+
<View>
88+
<Text>
89+
Name: {title} {first} {last}
90+
</Text>
91+
<Text>Email: {email}</Text>
92+
<Text>Mobile: {cell}</Text>
93+
</View>
94+
</View>
95+
);
96+
},
97+
[],
98+
);
99+
100+
if (users.length === 0) return (
101+
<View style={styles.loaderContainer}>
102+
<Text>Users data not quite there yet...</Text>
103+
</View>
104+
);
105+
106+
return (
107+
<View>
108+
<FlatList<User>
109+
data={users}
110+
renderItem={renderItem}
111+
keyExtractor={(item, index) => `${index}-${item.id.value}`}
112+
/>
113+
</View>
114+
);
115+
};
116+
117+
// Looking for styles?
118+
// Check examples/cookbook/app/network-requests/components/ContactsList.tsx
119+
const styles =
120+
...
121+
```
122+
123+
## Start testing with a simple test
124+
125+
In our test we will make sure we mock the `fetch` function to return the data we want.
126+
In this specific case, we will return a list of 3 users.
127+
As the `fetch` api is available globally, we can mock it by using `jest.spyOn` specifically on the
128+
`global` object.
129+
130+
:::info
131+
By using `mockResolvedValueOnce` we gain more grip and prevent the mock from resolving the data
132+
multiple times, which might lead to unexpected behavior.
133+
:::
134+
135+
```tsx title=network-requests/Phonebook.test.tsx
136+
import {render, waitForElementToBeRemoved} from '@testing-library/react-native';
137+
import React from 'react';
138+
import PhoneBook from '../PhoneBook';
139+
import {User} from '../types';
140+
141+
describe('PhoneBook', () => {
142+
it('fetches contacts successfully and renders in list', async () => {
143+
// mock the fetch function to return the data we want
144+
jest.spyOn(global, 'fetch').mockResolvedValueOnce({
145+
json: jest.fn().mockResolvedValueOnce(DATA),
146+
});
147+
render(<PhoneBook/>);
148+
149+
// Wait for the loader to disappear
150+
await waitForElementToBeRemoved(() => screen.getByText(/users data not quite there yet/i));
151+
// Check if the users are displayed
152+
expect(await screen.findByText('Name: Mrs Ida Kristensen')).toBeOnTheScreen();
153+
expect(await screen.findByText('Email: [email protected]')).toBeOnTheScreen();
154+
expect(await screen.findAllByText(/name/i)).toHaveLength(3);
155+
});
156+
});
157+
158+
const DATA: { results: User[] } = {
159+
results: [
160+
{
161+
name: {
162+
title: 'Mrs',
163+
first: 'Ida',
164+
last: 'Kristensen',
165+
},
166+
167+
id: {
168+
name: 'CPR',
169+
value: '250562-5730',
170+
},
171+
picture: {
172+
large: 'https://randomuser.me/api/portraits/women/26.jpg',
173+
medium: 'https://randomuser.me/api/portraits/med/women/26.jpg',
174+
thumbnail: 'https://randomuser.me/api/portraits/thumb/women/26.jpg',
175+
},
176+
cell: '123-4567-890',
177+
},
178+
// For brevity, we have omitted the rest of the users, you can still find them in
179+
// examples/cookbook/app/network-requests/__tests__/PhoneBook.test.tsx
180+
...
181+
],
182+
};
183+
184+
```
185+
186+
## Testing error handling
187+
188+
As we are dealing with network requests, we should also test how our application behaves when the
189+
API request fails. We can mock the `fetch` function to throw an error and/or mark it's response as
190+
not 'ok' in order to verify if our application is handling the error correctly.
191+
192+
:::note
193+
The `fetch` function will reject the promise on some errors, but not if the server responds
194+
with an error status like 404: so we also check the response status and throw if it is not OK.
195+
See MDN's [docs](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch) for more
196+
:::
197+
198+
```tsx title=network-requests/Phonebook.test.tsx
199+
...
200+
it('fails to fetch contacts and renders error message', async () => {
201+
// mock the fetch function to be not ok which will throw an error
202+
(global.fetch as jest.SpyInstance).mockResolvedValueOnce({
203+
ok: false,
204+
});
205+
render(<PhoneBook/>);
206+
207+
// Wait for the loader to disappear
208+
await waitForElementToBeRemoved(() => screen.getByText(/users data not quite there yet/i));
209+
// Check if the error message is displayed
210+
expect(await screen.findByText(/error fetching contacts/i)).toBeOnTheScreen();
211+
});
212+
````
213+
214+
## Global guarding against unwanted API requests
215+
216+
As mistakes may happen, we might forget to mock an API request in one of our tests in the future.
217+
To prevent we make unwanted API requests, and alert the developer when it happens, we can globally
218+
mock the `fetch` in our test suite via the `jest.setup.ts` file. Ensure this setup file is included
219+
in [`setupFilesAfterEnv`](https://jestjs.io/docs/configuration#setupfilesafterenv-array) in your Jest configuration.
220+
221+
```tsx title=jest.setup.ts
222+
beforeAll(() => {
223+
// the global fetch function:
224+
jest.spyOn(global, 'fetch').mockImplementation(() => {
225+
throw Error("Please ensure you mock 'fetch' Only Chuck Norris is allowed to make API requests when testing ;)");
226+
});
227+
});
228+
afterAll(() => {
229+
// restore the original fetch function
230+
(global.fetch as jest.Mock).mockRestore();
231+
});
232+
233+
```
234+
235+
## Conclusion
236+
237+
Testing a component that makes network requests with `fetch` is straightforward. By mocking the fetch
238+
requests, we can control the data that is returned and test how our application behaves in different
239+
scenarios, such as when the request is successful or when it fails.
240+
There are many ways to mock `fetch` requests, and the method you choose will depend on your specific
241+
use case. In this guide, we showed you how to mock `fetch` requests using Jest's `jest.spyOn` function
242+
and how to guard against unwanted API requests throughout your test suite.

0 commit comments

Comments
 (0)