Skip to content

Commit 336c00e

Browse files
author
stevegalili
committed
use MSW for all API calls in cookbook test suits
1 parent 7fa6eb0 commit 336c00e

File tree

7 files changed

+379
-140
lines changed

7 files changed

+379
-140
lines changed

examples/cookbook/__mocks__/axios.ts

Lines changed: 0 additions & 14 deletions
This file was deleted.

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

Lines changed: 8 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,48 +2,34 @@ import { render, screen, waitForElementToBeRemoved } from '@testing-library/reac
22
import React from 'react';
33
import PhoneBook from '../PhoneBook';
44
import {
5-
mockAxiosGetWithFailureResponse,
6-
mockAxiosGetWithSuccessResponse,
7-
mockFetchWithFailureResponse,
8-
mockFetchWithSuccessResponse,
5+
mockServerFailureForGetAllContacts,
6+
mockServerFailureForGetAllFavorites,
97
} from './test-utils';
108

119
jest.setTimeout(10000);
1210

1311
describe('PhoneBook', () => {
14-
it('fetches contacts successfully and renders in list', async () => {
15-
mockFetchWithSuccessResponse();
16-
mockAxiosGetWithSuccessResponse();
12+
it('fetches all contacts and favorites successfully and renders lists in sections correctly', async () => {
1713
render(<PhoneBook />);
1814

1915
await waitForElementToBeRemoved(() => screen.getByText(/users data not quite there yet/i));
2016
expect(await screen.findByText('Name: Mrs Ida Kristensen')).toBeOnTheScreen();
2117
expect(await screen.findByText('Email: [email protected]')).toBeOnTheScreen();
2218
expect(await screen.findAllByText(/name/i)).toHaveLength(3);
19+
expect(await screen.findByText(/my favorites/i)).toBeOnTheScreen();
20+
expect(await screen.findAllByLabelText('favorite-contact-avatar')).toHaveLength(3);
2321
});
2422

25-
it('fails to fetch contacts and renders error message', async () => {
26-
mockFetchWithFailureResponse();
27-
mockAxiosGetWithSuccessResponse();
23+
it('fails to fetch all contacts and renders error message', async () => {
24+
mockServerFailureForGetAllContacts();
2825
render(<PhoneBook />);
2926

3027
await waitForElementToBeRemoved(() => screen.getByText(/users data not quite there yet/i));
3128
expect(await screen.findByText(/error fetching contacts/i)).toBeOnTheScreen();
3229
});
3330

34-
it('fetches favorites successfully and renders all users avatars', async () => {
35-
mockFetchWithSuccessResponse();
36-
mockAxiosGetWithSuccessResponse();
37-
render(<PhoneBook />);
38-
39-
await waitForElementToBeRemoved(() => screen.getByText(/figuring out your favorites/i));
40-
expect(await screen.findByText(/my favorites/i)).toBeOnTheScreen();
41-
expect(await screen.findAllByLabelText('favorite-contact-avatar')).toHaveLength(3);
42-
});
43-
4431
it('fails to fetch favorites and renders error message', async () => {
45-
mockFetchWithSuccessResponse();
46-
mockAxiosGetWithFailureResponse();
32+
mockServerFailureForGetAllFavorites();
4733
render(<PhoneBook />);
4834

4935
await waitForElementToBeRemoved(() => screen.getByText(/figuring out your favorites/i));

examples/cookbook/app/network-requests/__tests__/test-utils.ts

Lines changed: 40 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,52 @@
11
import { User } from '../types';
2-
import axios from 'axios';
2+
import {http, HttpResponse} from "msw";
3+
import {setupServer} from "msw/node";
34

4-
class MismatchedUrlError extends Error {
5-
constructor(url: string) {
6-
super(`The URL: ${url} does not match the API's base URL.`);
7-
}
8-
}
5+
const handlers = [
6+
http.get('https://randomuser.me/api/*', () => {
7+
return HttpResponse.json(DATA);
8+
}),
9+
];
910

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-
};
11+
export const server = setupServer(...handlers);
2912

30-
export const mockFetchWithFailureResponse = () => {
31-
(global.fetch as jest.Mock).mockImplementationOnce((url) => {
32-
ensureUrlMatchesBaseUrl(url);
13+
export const mockServerFailureForGetAllContacts = () => {
14+
server.use(
15+
http.get('https://randomuser.me/api/', ({ request }) => {
16+
// Construct a URL instance out of the intercepted request.
17+
const url = new URL(request.url);
18+
// Read the "results" URL query parameter using the "URLSearchParams" API.
19+
const resultsLength = url.searchParams.get('results');
20+
// Simulate a server error for the get all contacts request.
21+
// We check if the "results" query parameter is set to "25"
22+
// to know it's the correct request to mock, in our case get all contacts.
23+
if (resultsLength === '25') {
24+
return new HttpResponse(null, { status: 500 });
25+
}
3326

34-
return Promise.resolve({
35-
ok: false,
36-
});
37-
});
27+
return HttpResponse.json(DATA);
28+
}),
29+
);
3830
};
3931

40-
export const mockAxiosGetWithSuccessResponse = () => {
41-
(axios.get as jest.Mock).mockImplementationOnce((url) => {
42-
ensureUrlMatchesBaseUrl(url);
32+
export const mockServerFailureForGetAllFavorites = () => {
33+
server.use(
34+
http.get('https://randomuser.me/api/', ({ request }) => {
35+
// Construct a URL instance out of the intercepted request.
36+
const url = new URL(request.url);
37+
// Read the "results" URL query parameter using the "URLSearchParams" API.
38+
const resultsLength = url.searchParams.get('results');
39+
// Simulate a server error for the get all favorites request.
40+
// We check if the "results" query parameter is set to "10"
41+
// to know it's the correct request to mock, in our case get all favorites.
42+
if (resultsLength === '10') {
43+
return new HttpResponse(null, { status: 500 });
44+
}
4345

44-
return Promise.resolve({ data: DATA });
45-
});
46+
return HttpResponse.json(DATA);
47+
}),
48+
);
4649
};
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-
5650
export const DATA: { results: User[] } = {
5751
results: [
5852
{
Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1-
import axios from 'axios';
21
import { User } from '../types';
32

43
export default async (): Promise<User[]> => {
5-
const res = await axios.get('https://randomuser.me/api/?results=10');
6-
return res.data.results;
4+
const res = await fetch('https://randomuser.me/api/?results=10');
5+
if (!res.ok) {
6+
throw new Error(`Error fetching favorites`);
7+
}
8+
const json = await res.json();
9+
return json.results;
710
};

examples/cookbook/jest-setup.ts

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,11 @@
22

33
// Import built-in Jest matchers
44
import '@testing-library/react-native/extend-expect';
5+
import { server } from './app/network-requests/__tests__/test-utils';
56

67
// Silence the warning: Animated: `useNativeDriver` is not supported because the native animated module is missing
78
jest.mock('react-native/Libraries/Animated/NativeAnimatedHelper');
89

9-
// Guard against API requests made during testing
10-
beforeAll(() => {
11-
// the global fetch function:
12-
jest.spyOn(global, 'fetch').mockImplementation(()=> {
13-
throw Error("Please ensure you mock 'fetch' Only Chuck Norris is allowed to make API requests when testing ;)");
14-
});
15-
// with Axios:
16-
// see examples/cookbook/__mocks__/axios.ts
17-
});
18-
afterAll(() => {
19-
// restore the original fetch function
20-
(global.fetch as jest.Mock).mockRestore();
21-
});
10+
beforeAll(() => server.listen());
11+
afterEach(() => server.resetHandlers());
12+
afterAll(() => server.close());

examples/cookbook/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
"typecheck": "tsc --noEmit"
1212
},
1313
"dependencies": {
14-
"axios": "^1.7.5",
1514
"expo": "^51.0.26",
1615
"expo-constants": "~16.0.2",
1716
"expo-linking": "~6.3.1",
@@ -38,6 +37,7 @@
3837
"@types/react-native-get-random-values": "^1",
3938
"eslint": "^8.57.0",
4039
"jest": "^29.7.0",
40+
"msw": "^2.4.4",
4141
"react-test-renderer": "18.2.0",
4242
"typescript": "~5.3.3"
4343
},

0 commit comments

Comments
 (0)