Тестирование компонентов с запросом в rtk-query с помощью msw и react-testing-library.
Привет всем, я начал тестировать веб-приложение react, и мои запросы на получение и загрузку данных выполняются с помощью rtk-query. Я расскажу вам, как писать тесты для компонентов при использовании rtk-запросов.
Сначала ознакомьтесь с моим руководством по настройке rtk query в redux toolkit.
Вы можете использовать библиотеку типа msw для имитации вашего api — именно это мы используем в кодовой базе Redux Toolkit для тестирования RTK Query.
phryneas (мейнтейнер инструментария Redux)
npm install msw --save-dev
Чтобы протестировать RTK Query с помощью библиотеки тестирования react, нужно выполнить три шага,
- используйте
msw
для имитации вашего API. - оберните ваш компонент в реальный магазин Redux с вашим API.
- напишите свои тесты — используйте что-то для ожидания изменений пользовательского интерфейса.
Настройка пользовательской функции рендеринга
Нам нужна пользовательская функция рендеринга, чтобы обернуть наши компоненты при тестировании. Эта функция называется renderWithProviders
Узнать больше
// ./src/test-utils.js
import React from 'react'
import { render } from '@testing-library/react'
import { Provider } from 'react-redux'
import { setupStore } from './app/store'
import { setupListeners } from '@reduxjs/toolkit/dist/query'
export function renderWithProviders(
ui,
{
preloadedState = {},
// Automatically create a store instance if no store was passed in
store = setupStore(preloadedState),
...renderOptions
} = {}
) {
setupListeners(store.dispatch);
function Wrapper({ children }) {
return <Provider store={store}>{children}</Provider>
}
return { store, ...render(ui, { wrapper: Wrapper, ...renderOptions }) }
}
Магазин Redux
Мы настроим наш redux store немного по-другому, подробнее об этом читайте здесь
// ./src/app/store.js
import { configureStore } from "@reduxjs/toolkit";
import { apiSlice } from "./api/apiSlice";
export const setupStore = preloadedState => {
return configureStore({
reducer: {
[apiSlice.reducerPath]: apiSlice.reducer,
},
preloadedState,
middleware: getDefaultMiddleware =>
getDefaultMiddleware({
immutableCheck: false,
serializableCheck: false,
}).concat(apiSlice.middleware),
})
}
Предоставление магазина приложению
Нам нужно обернуть наше react-приложение с настроенным нами redux store
// ./src/index.js
import { setupStore } from './app/store'
import { Provider } from 'react-redux';
const store = setupStore({});
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);
Настройка тестового окружения
Прежде чем мы начнем, мы должны настроить наше тестовое окружение на JEST
setupTests.js
// ./src/setupTests.js
import '@testing-library/jest-dom';
import { server } from './mocks/api/server'
import { apiSlice } from './app/api/apiSlice'
import { setupStore } from './app/store'
const store = setupStore({});
// Establish API mocking before all tests.
beforeAll(() => {
server.listen();
});
// Reset any request handlers that we may add during the tests,
// so they don't affect other tests.
afterEach(() => {
server.resetHandlers();
// This is the solution to clear RTK Query cache after each test
store.dispatch(apiSlice.util.resetApiState());
});
// Clean up after the tests are finished.
afterAll(() => server.close());
Мы сбрасываем API между тестами, поскольку API также имеет внутреннее состояние, вызывая store.dispatch(apiSlice.util.resetApiState());
после каждого теста.
Мокинг REST API
Мы используем msw
для имитации (mock) запросов API, которые мы делаем в нашем приложении. Я покажу вам, как настроить и использовать msw
.
msw
В каталоге src
создайте папку mock
и вложенную папку api
.
Обработчик API
Обработчик содержит глобальную настройку для успешного запроса, если API был сымитирован (запрошен) успешно, ответ будет взят из того, что мы определили в объекте ответа msw
.
./src/mock/api/handler.js
// ./src/mock/api/handler.js
import { rest } from 'msw'
export const handlers = [
rest.get('https://jsonplaceholder.typicode.com/users', (req, res, ctx) => {
// successful response
return res(ctx.status(200), ctx.json([
{ id: 1, name: 'Xabi Alonzo' },
{ id: 2, name: 'Lionel Messi' },
{ id: 3, name: 'Lionel Love' },
{ id: 4, name: 'Lionel Poe' },
{ id: 5, name: 'Lionel Gink' },
]), ctx.delay(30))
})
]
./src/mock/api/server.js
// ./src/mock/api/server.js
import { setupServer } from 'msw/node'
import {handlers} from "./handler"
export const server = setupServer(...handlers)
Наконец, пишем тесты
Тест 1: Выборка из API
Чтобы обработать запрос REST API, нам нужно указать его метод, путь и функцию, которая вернет насмешливый ответ. узнать больше.
Вот наша структура URL:
baseUrl: "https://api.coingecko.com/api/v3"
.
Параметры запроса: ?vs_currency=ngn&order=market_cap_desc&per_page=100&page=1
.
Перехваченный запрос — это запрос, который мы делаем в нашем компоненте.
const queryRequest = {
vs_currency: "usd",
order: "market_cap_desc",
per_page: "10",
sparkline: "false",
page
}
const {
data: coins,
isSuccess,
isError,
error,
isLoading
} = useGetCoinsQuery(queryRequest)
getCoins: builder.query({
query: (arg) => ({
url: `/coins/markets`,
params: {...arg}
}),
providesTags: ["coins"],
})
Тест; получение данных из API
// ./src/__test__/Crypto.test.js
const apiData = [
{name: "Mark Zuckerberg", age: "34"},
{name: "Elon Musk", age: "44"}
]
test("table should render after fetching from API depending on request Query parameters", async () => {
// custom msw server
server.use(
rest.get(`*`, (req, res, ctx) => {
const arg = req.url.searchParams.getAll("page");
console.log(arg)
return res(ctx.json(apiData))
}
)
);
// specify table as the render container
const table = document.createElement('table')
// wrap component with custom render function
const { container } = renderWithProviders(<Coins />, {
container: document.body.appendChild(table),
});
const allRows = await screen.findAllByRole("row")
await waitFor(() => {
expect(container).toBeInTheDocument();
})
await waitFor(() => {
expect(allRows.length).toBe(10);
})
})
объяснение теста
- создайте пользовательский сервер :- Для каждого теста мы можем переопределить обработчик API для тестирования отдельных сценариев, создав пользовательский сервер
msw
. - apiData :- это ответ, который мы ожидаем получить от API.
- обернуть таблицу контейнером :- согласно документации RTL (react testing library), нам нужно указать table в качестве контейнера рендеринга.
- обернуть компонент :- мы обернем компонент, который хотим протестировать, нашей пользовательской функцией reder.
- подстановочный знак (*) :- мы используем его для представления api URL.
- get all
tr
element :- Я хочу получить всеtr
element, чтобы проверить, есть ли у нас до 10 строк в таблице. Для этого я используюrow
, подробнее можно узнать здесь
Тест 2: Подражание ответам на ошибки
Если вы хотите написать тест для сценария ошибки, например, когда сервер API недоступен.
Перехваченный запрос
{isError && (<p data-testid="error" className="text-center text-danger">Oh no, there was an error {JSON.stringify(error.error)} </p>)}
{isError && (<p data-testid="customError" className="text-center text-danger">{error.data.message}</p>)}
Тест; высмеивание ошибки sceneraio
// ./src/__test__/Crypto.test.js
test('renders error message if API fails on page load', async () => {
server.use(
rest.get('*', (_req, res, ctx) =>
res.once(ctx.status(500), ctx.json({message: "baby, there was an error"}))
)
);
renderWithProviders(<Coins />);
const errorText = await screen.findByTestId("error");
const errorMessage = await screen.findByTestId("customError");
await waitFor(() => {
expect(errorMessage.textContent).toBe("baby, there was an error")
})
await waitFor(() => {
expect(errorText.textContent).toBe("Oh no, there was an error");
})
});
объяснение теста
- создание пользовательского сервера :- Для каждого теста мы можем переопределить обработчик API для тестирования отдельных сценариев, создав пользовательский сервер
msw
. - Нам не нужен аргумент req
argument
, потому что мы тестируем на ошибки. - обернуть компонент :- мы обернем компонент, который хотим протестировать, нашей пользовательской функцией reder.
- подстановочный знак (*) :- мы используем его для представления URL API.
- res status code :- мы намеренно выбрасываем ошибку с кодом состояния (500) для проверки на наличие ошибки.
- тело ответа :- мы передаем сообщение об ошибке как объект в тело ответа.
ссылка
Тестирование компонентов с помощью запроса rtk-query
Тестирование React-query с помощью MSW