Мы будем использовать Mock Service Worker для имитации API. Поэтому вместо прямого вызова API или имитации window.fetch лучше имитировать поведение сервера.
Это дает нам песочницу для игры с сервером API без загрязнения window.fetch и без необходимости устанавливать тестовый сервер API для тестирования.
Спасибо Артему Захарченко, который сделал полезный инструмент.
Введение
Вы создали красивую функцию Feed на своем сайте и теперь хотите протестировать эту функцию.
Ваш код выглядит следующим образом
import React from 'react';
function Feed() {
const [feeds, setFeeds] = React.useState([]);
React.useEffect(() => {
const ctl = new AbortController();
fetch('https://api.domain.com/feeds')
.then(res => res.json())
.then(res => setFeeds(res.data))
.catch(() => {
// Log the error
});
return cleanup() {
ctl.abort();
};
}, [])
return (
<ul>
{feeds.map(feed => (
<li>{feed.title}</li>
))}
</ul>
);
}
Хотя, это действительно простая задача, где вы хотите показать пользователям фид в пользовательском интерфейсе.
Вы запутались в том, как протестировать функцию, потому что вы ”должны” получить данные фида из API, но поскольку это тест, вы не будете обращаться непосредственно к API.
Путешествие
Теперь вы ищете возможные решения в Интернете. Поскольку вы используете React и, конечно же, используете Jest и testing-library (это стандартная настройка от CRA), кто-то предлагает вам поиздеваться над объектом window.fetch.
Вы заинтересовались и вставили решение в свой тест следующим образом
// Import all dependencies...
const MOCK_FEEDS = [/** Feed objects here */];
beforeAll(() => {
jest.spyOn(window, 'fetch');
});
describe('Feed Feature Test', () => {
it('should shows users feed', () => {
window.fetch.mockResolvedValueOnce({
ok: true,
json: async () => ({ data: MOCK_FEEDS }),
})
// Your testing goes here...
});
});
Вы тестируете код, и он выглядит зеленым (значит, тест пройден), и вы довольны этим.
Наступило завтра. Вы забыли о пользовательском интерфейсе ”пустое состояние” для функции Feed, и сегодня вы его сделали.
Поскольку UI с пустым состоянием должен выдавать ошибку HTTP NOT FOUND, вам нужно переделать тест.
// Import all dependencies...
const MOCK_FEEDS = [/** Feed objects here */];
const MOCK_FEEDS_NOT_FOUND = [];
beforeAll(() => {
jest.spyOn(window, 'fetch');
});
beforeEach(() => {
jest.restoreAllMocks()
});
describe('Feed Feature Test', () => {
it('should shows empty state UI when feeds not found', () => {
window.fetch.mockResolvedValueOnce({
ok: false,
status: 404,
json: async () => ({ data: MOCK_FEEDS_NOT_FOUND }),
})
// Your testing goes here...
});
it('should shows users feed', () => {
window.fetch.mockResolvedValueOnce({
ok: false,
json: async () => ({ data: MOCK_FEEDS }),
})
// Your testing goes here...
});
});
Ваш тест пройден, и вы снова счастливы.
На следующее утро вам позвонил менеджер проекта. Он хочет, чтобы вы добавили кнопку “like (❤️)”, и вы делаете это.
В рамках этой задачи вы должны обновить функцию, чтобы получить две конечные точки API. Сначала вы должны использовать GET /feeds, а затем POST /feeds/{id}/like для выполнения функции “like this feed”.
Добавив несколько кодов, вы снова рефакторите тест, но понимаете, что имитируемая выборка не заботится об URL. К какой бы конечной точке API вы ни обращались, она всегда возвращает один и тот же ответ и потенциальную ошибку на будущее.
Теперь тест выглядит следующим образом
// Import all dependencies...
const MOCK_FEEDS = [/** Feed objects here */];
const MOCK_FEEDS_NOT_FOUND = [];
beforeAll(() => {
jest.spyOn(window, 'fetch');
});
beforeEach(() => {
window.fetch.mockImplementation(async (url, config) => {
switch (url) {
case '/feeds':
return {
ok: true,
json: async () => MOCK_FEEDS,
};
case '/feeds/1/like':
return {
ok: true,
json: async () => ({ liked: true })
};
default:
throw new Error(`Unhandled request: ${url}`);
}
});
});
describe('Feed Feature Test', () => {
it('should shows empty state UI when feeds not found', () => {
// Your testing goes here...
});
it('should shows users feed', () => {
// Your testing goes here...
});
it('should able to like the feed', () => {
// Your testing goes here...
})
});
Тесты пройдены, кроме первого. Вы спрашиваете ”почему?”, а затем понимаете, что нет обработчика запроса для GET /feeds со статусом 404 http.
Решение
К счастью, существует инструмент под названием Mock Service Worker. Это инструмент для имитации API и работы на сетевом уровне.
Просто представьте, что вы можете подражать вашему API серверу и использовать его внутри вашего теста без головной боли.
И самое интересное:
Поддержка Rest API и GraphQL
Поддержка Node и Browser (то есть вы можете использовать его с Express и т.д.).
И многое другое…
Хватит объяснять, давайте установим требования и отрефакторим тесты.
Во-первых, установите msw.
npm i -D msw
Чтобы использовать MSW, вам нужно сделать обработчики запросов с определенным методом HTTP и URL. Так, он дает вам возможность сделать GET 200 /feeds и GET 404 /feeds. Он также поддерживает параметр URL, такой как /feeds/:feedId/like.
Теперь обновите тест следующим образом…
// Import all dependencies...
import { rest } from 'msw';
import { setupServer } from 'msw/node';
const MOCK_FEEDS = [/** Feed objects here */];
const MOCK_FEEDS_NOT_FOUND = [];
const url = (path) => `https://api.domain.com/feeds/${path}`;
const defaultHandlers = [
rest.get(url(path), (req, res, ctx) => {
return res(ctx.json(MOCK_FEEDS));
}),
rest.get(url(path), () => {
return res(
ctx.status(404),
ctx.json(MOCK_FEEDS_NOT_FOUND),
);
})
];
const server = setupServer(...defaultHandlers);
describe('Feed Feature Test', () => {
beforeAll(() => {
server.listen();
});
afterEach(() => {
server.resetHandlers();
});
afterAll(() => {
server.close();
});
it('should shows empty state UI when feeds not found', () => {
// Your testing goes here...
});
it('should shows users feed', () => {
// Your testing goes here...
});
it('should able to like the feed', () => {
// Adds the "POST /feeds/:feedId/like" request handler as a part of this test.
server.use(
rest.post(url('feeds/:feedId/like'), (req, rest, ctx) => {
return res(
ctx.json({ liked: true }),
);
}),
);
// Your testing goes here...
})
});
Бум! Ваши тесты пройдены. Не только тесты пройдены, но и улучшен DX, потому что вы можете легко создать мощную маршрутизацию URL.
Вы также можете разделить обработчики в другой файл, чтобы избежать повторного написания обработчиков. Это делает их многократно используемыми во всех тестах.
MSW не только обеспечивает лучший DX, но и предлагает вам HTTP Cookie и другие возможности HTTP. Потрясающе!
Заключение
Издевательство (и тестирование) HTTP-запросов - одна из сложных задач, но MSW предлагает мощные возможности, кросс-платформенность и простой подход к решению этой проблемы.
MSW обеспечивает нативный подход, перехватывая сеть и возвращая созданные нами обработчики вместо настоящих. Это предотвращает загрязнение window.fetch путем издевательства над ним и потенциально может привести к ошибкам.
Бесшовная библиотека мокинга REST/GraphQL API для браузера и Node.js.
Mock Service Worker (MSW) - это бесшовная библиотека мокинга REST/GraphQL API для браузера и Node.js.
Особенности
Бесшовность. Специальный уровень перехвата запросов в вашем распоряжении. Код и тесты вашего приложения не будут знать о том, высмеивается что-то или нет.
Без отклонений. Запрашивайте те же производственные ресурсы и тестируйте реальное поведение вашего приложения. Дополняйте существующий API или разрабатывайте его на ходу, когда его нет.
Знакомый и мощный. Используйте синтаксис маршрутизации, подобный Express, для перехвата запросов. Используйте параметры, подстановочные знаки и регулярные выражения для сопоставления запросов и отвечайте на них необходимыми кодами состояния, заголовками, cookies, задержками или полностью собственными резолверами.
”Я нашел MSW и был в восторге от того, что я не только мог видеть имитированные ответы в своих DevTools, но и от того, что имитаторы не нужно было писать в Service Worker, а можно было жить рядом с остальными приложениями.