Skip to content

Commit 4a2fb86

Browse files
author
stevegalili
committed
Add url check to mock and further reading and alternatives
1 parent 5a5cab2 commit 4a2fb86

File tree

5 files changed

+205
-95
lines changed

5 files changed

+205
-95
lines changed

examples/cookbook/__mocks__/axios.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const chuckNorrisError = () => {
55
};
66

77
export default {
8+
...jest.requireActual('axios'),
89
get: jest.fn(chuckNorrisError),
910
post: jest.fn(chuckNorrisError),
1011
put: jest.fn(chuckNorrisError),
Lines changed: 14 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
import { render, screen, waitForElementToBeRemoved } from '@testing-library/react-native';
22
import React from 'react';
3-
import axios from 'axios';
43
import PhoneBook from '../PhoneBook';
5-
import { User } from '../types';
4+
import {
5+
mockAxiosGetWithFailureResponse,
6+
mockAxiosGetWithSuccessResponse,
7+
mockFetchWithFailureResponse,
8+
mockFetchWithSuccessResponse,
9+
} from './test-utils';
610

711
jest.setTimeout(10000);
812

913
describe('PhoneBook', () => {
1014
it('fetches contacts successfully and renders in list', async () => {
11-
(global.fetch as jest.Mock).mockResolvedValueOnce({
12-
ok: true,
13-
json: jest.fn().mockResolvedValueOnce(DATA),
14-
});
15-
(axios.get as jest.Mock).mockResolvedValue({ data: DATA });
15+
mockFetchWithSuccessResponse();
16+
mockAxiosGetWithSuccessResponse();
1617
render(<PhoneBook />);
1718

1819
await waitForElementToBeRemoved(() => screen.getByText(/users data not quite there yet/i));
@@ -22,22 +23,17 @@ describe('PhoneBook', () => {
2223
});
2324

2425
it('fails to fetch contacts and renders error message', async () => {
25-
(global.fetch as jest.Mock).mockResolvedValueOnce({
26-
ok: false,
27-
});
28-
(axios.get as jest.Mock).mockResolvedValue({ data: DATA });
26+
mockFetchWithFailureResponse();
27+
mockAxiosGetWithSuccessResponse();
2928
render(<PhoneBook />);
3029

3130
await waitForElementToBeRemoved(() => screen.getByText(/users data not quite there yet/i));
3231
expect(await screen.findByText(/error fetching contacts/i)).toBeOnTheScreen();
3332
});
3433

3534
it('fetches favorites successfully and renders all users avatars', async () => {
36-
(global.fetch as jest.Mock).mockResolvedValueOnce({
37-
ok: true,
38-
json: jest.fn().mockResolvedValueOnce(DATA),
39-
});
40-
(axios.get as jest.Mock).mockResolvedValue({ data: DATA });
35+
mockFetchWithSuccessResponse();
36+
mockAxiosGetWithSuccessResponse();
4137
render(<PhoneBook />);
4238

4339
await waitForElementToBeRemoved(() => screen.getByText(/figuring out your favorites/i));
@@ -46,73 +42,11 @@ describe('PhoneBook', () => {
4642
});
4743

4844
it('fails to fetch favorites and renders error message', async () => {
49-
(global.fetch as jest.Mock).mockResolvedValueOnce({
50-
ok: true,
51-
json: jest.fn().mockResolvedValueOnce(DATA),
52-
});
53-
(axios.get as jest.Mock).mockRejectedValueOnce({ message: 'Error fetching favorites' });
45+
mockFetchWithSuccessResponse();
46+
mockAxiosGetWithFailureResponse();
5447
render(<PhoneBook />);
5548

5649
await waitForElementToBeRemoved(() => screen.getByText(/figuring out your favorites/i));
5750
expect(await screen.findByText(/error fetching favorites/i)).toBeOnTheScreen();
5851
});
5952
});
60-
61-
const DATA: { results: User[] } = {
62-
results: [
63-
{
64-
name: {
65-
title: 'Mrs',
66-
first: 'Ida',
67-
last: 'Kristensen',
68-
},
69-
70-
id: {
71-
name: 'CPR',
72-
value: '250562-5730',
73-
},
74-
picture: {
75-
large: 'https://randomuser.me/api/portraits/women/26.jpg',
76-
medium: 'https://randomuser.me/api/portraits/med/women/26.jpg',
77-
thumbnail: 'https://randomuser.me/api/portraits/thumb/women/26.jpg',
78-
},
79-
cell: '123-4567-890',
80-
},
81-
{
82-
name: {
83-
title: 'Mr',
84-
first: 'Elijah',
85-
last: 'Ellis',
86-
},
87-
88-
id: {
89-
name: 'TFN',
90-
value: '138117486',
91-
},
92-
picture: {
93-
large: 'https://randomuser.me/api/portraits/men/53.jpg',
94-
medium: 'https://randomuser.me/api/portraits/med/men/53.jpg',
95-
thumbnail: 'https://randomuser.me/api/portraits/thumb/men/53.jpg',
96-
},
97-
cell: '123-4567-890',
98-
},
99-
{
100-
name: {
101-
title: 'Mr',
102-
first: 'Miro',
103-
last: 'Halko',
104-
},
105-
106-
id: {
107-
name: 'HETU',
108-
value: 'NaNNA945undefined',
109-
},
110-
picture: {
111-
large: 'https://randomuser.me/api/portraits/men/17.jpg',
112-
medium: 'https://randomuser.me/api/portraits/med/men/17.jpg',
113-
thumbnail: 'https://randomuser.me/api/portraits/thumb/men/17.jpg',
114-
},
115-
cell: '123-4567-890',
116-
},
117-
],
118-
};
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { User } from '../types';
2+
import axios from 'axios';
3+
4+
class MismatchedUrlError extends Error {
5+
constructor(url: string) {
6+
super(`The URL: ${url} does not match the API's base URL.`);
7+
}
8+
}
9+
10+
/**
11+
* Ensures that the URL matches the base URL of the API.
12+
* @param url
13+
* @throws {MismatchedUrlError}
14+
*/
15+
const ensureUrlMatchesBaseUrl = (url: string) => {
16+
if (!url.includes('https://randomuser.me/api')) throw new MismatchedUrlError(url);
17+
};
18+
19+
export const mockFetchWithSuccessResponse = () => {
20+
(global.fetch as jest.Mock).mockImplementationOnce((url) => {
21+
ensureUrlMatchesBaseUrl(url);
22+
23+
return Promise.resolve({
24+
ok: true,
25+
json: jest.fn().mockResolvedValueOnce(DATA),
26+
});
27+
});
28+
};
29+
30+
export const mockFetchWithFailureResponse = () => {
31+
(global.fetch as jest.Mock).mockImplementationOnce((url) => {
32+
ensureUrlMatchesBaseUrl(url);
33+
34+
return Promise.resolve({
35+
ok: false,
36+
});
37+
});
38+
};
39+
40+
export const mockAxiosGetWithSuccessResponse = () => {
41+
(axios.get as jest.Mock).mockImplementationOnce((url) => {
42+
ensureUrlMatchesBaseUrl(url);
43+
44+
return Promise.resolve({ data: DATA });
45+
});
46+
};
47+
48+
export const mockAxiosGetWithFailureResponse = () => {
49+
(axios.get as jest.Mock).mockImplementationOnce((url) => {
50+
ensureUrlMatchesBaseUrl(url);
51+
52+
return Promise.reject({ message: 'Error fetching favorites' });
53+
});
54+
};
55+
56+
export const DATA: { results: User[] } = {
57+
results: [
58+
{
59+
name: {
60+
title: 'Mrs',
61+
first: 'Ida',
62+
last: 'Kristensen',
63+
},
64+
65+
id: {
66+
name: 'CPR',
67+
value: '250562-5730',
68+
},
69+
picture: {
70+
large: 'https://randomuser.me/api/portraits/women/26.jpg',
71+
medium: 'https://randomuser.me/api/portraits/med/women/26.jpg',
72+
thumbnail: 'https://randomuser.me/api/portraits/thumb/women/26.jpg',
73+
},
74+
cell: '123-4567-890',
75+
},
76+
{
77+
name: {
78+
title: 'Mr',
79+
first: 'Elijah',
80+
last: 'Ellis',
81+
},
82+
83+
id: {
84+
name: 'TFN',
85+
value: '138117486',
86+
},
87+
picture: {
88+
large: 'https://randomuser.me/api/portraits/men/53.jpg',
89+
medium: 'https://randomuser.me/api/portraits/med/men/53.jpg',
90+
thumbnail: 'https://randomuser.me/api/portraits/thumb/men/53.jpg',
91+
},
92+
cell: '123-4567-890',
93+
},
94+
{
95+
name: {
96+
title: 'Mr',
97+
first: 'Miro',
98+
last: 'Halko',
99+
},
100+
101+
id: {
102+
name: 'HETU',
103+
value: 'NaNNA945undefined',
104+
},
105+
picture: {
106+
large: 'https://randomuser.me/api/portraits/men/17.jpg',
107+
medium: 'https://randomuser.me/api/portraits/med/men/17.jpg',
108+
thumbnail: 'https://randomuser.me/api/portraits/thumb/men/17.jpg',
109+
},
110+
cell: '123-4567-890',
111+
},
112+
],
113+
};

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

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -116,9 +116,11 @@ const styles =
116116
In our test we will make sure we mock the `axios.get` function to return the data we want.
117117
In this specific case, we will return a list of 3 users.
118118

119-
::info
120-
By using `mockResolvedValueOnce` we gain more grip and prevent the mock from resolving the data
121-
multiple times, which might lead to unexpected behavior.
119+
:::info
120+
To prevent unexpected behavior, we ensure the following:
121+
- Prevent the mock from resolving data multiple times by using `mockResolvedValueOnce`.
122+
- Ensure the URL matches the base URL of the API by using a custom function `ensureUrlMatchesBaseUrl`.
123+
122124
:::
123125

124126
```tsx title=network-requests/Phonebook.test.tsx
@@ -127,13 +129,25 @@ import React from 'react';
127129
import PhoneBook from '../PhoneBook';
128130
import {User} from '../types';
129131
import axios from 'axios';
132+
import {MismatchedUrlError} from './test-utils';
133+
134+
const ensureUrlMatchesBaseUrl = (url: string) => {
135+
if (!url.includes('https://randomuser.me/api')) throw new MismatchedUrlError(url);
136+
};
130137

131-
jest.mock('axios');
138+
export const mockAxiosGetWithSuccessResponse = () => {
139+
(axios.get as jest.Mock).mockImplementationOnce((url) => {
140+
// Ensure the URL matches the base URL of the API
141+
ensureUrlMatchesBaseUrl(url);
142+
143+
return Promise.resolve({ data: DATA });
144+
});
145+
};
132146

133147
describe('PhoneBook', () => {
134148
it('fetches favorites successfully and renders all users avatars', async () => {
135149
// Mock the axios.get function to return the data we want
136-
(axios.get as jest.Mock).mockResolvedValueOnce({data: DATA});
150+
mockAxiosGetWithSuccessResponse();
137151
render(<PhoneBook/>);
138152

139153
// Wait for the loader to disappear
@@ -183,9 +197,18 @@ It is good to note that Axios throws auto. an error when the response status cod
183197

184198
```tsx title=network-requests/Phonebook.test.tsx
185199
...
200+
201+
export const mockAxiosGetWithFailureResponse = () => {
202+
(axios.get as jest.Mock).mockImplementationOnce((url) => {
203+
ensureUrlMatchesBaseUrl(url);
204+
205+
return Promise.reject({ message: 'Error fetching favorites' });
206+
});
207+
};
208+
186209
it('fails to fetch favorites and renders error message', async () => {
187210
// Mock the axios.get function to throw an error
188-
(axios.get as jest.Mock).mockRejectedValueOnce({ message: 'Error fetching favorites' });
211+
mockAxiosGetWithFailureResponse();
189212
render(<PhoneBook />);
190213

191214
// Wait for the loader to disappear
@@ -225,3 +248,9 @@ scenarios, such as when the request is successful or when it fails.
225248
There are many ways to mock Axios requests, and the method you choose will depend on your specific
226249
use case. In this guide, we showed you how to mock Axios requests using Jest's `jest.mock` function
227250
and how to guard against unwanted API requests throughout your test suite.
251+
252+
## Further Reading and Alternatives
253+
254+
Explore more powerful tools for mocking network requests in your React Native application:
255+
- [Axios Mock Adapter](https://github.com/ctimmerm/axios-mock-adapter): A popular library for mocking Axios calls with an extensive API, making it easy to simulate various scenarios.
256+
- [MSW (Mock Service Worker)](https://mswjs.io/): Great for spinning up a local test server that intercepts network requests at the network level, providing end-to-end testing capabilities.

0 commit comments

Comments
 (0)