Как устранить предупреждение "обновление не было обернуто в act()" в Test-Library-React?
Я работаю с простым компонентом, у которого есть побочный эффект. Мой тест пройден, но я получаю предупреждениеWarning: An update to Hello inside a test was not wrapped in act(...).
.
Я тоже не знаю waitForElement
это лучший способ написать этот тест.
Мой компонент
export default function Hello() {
const [posts, setPosts] = useState([]);
useEffect(() => {
const fetchData = async () => {
const response = await axios.get('https://jsonplaceholder.typicode.com/posts');
setPosts(response.data);
}
fetchData();
}, []);
return (
<div>
<ul>
{
posts.map(
post => <li key={post.id}>{post.title}</li>
)
}
</ul>
</div>
)
}
Мой компонентный тест
import React from 'react';
import {render, cleanup, act } from '@testing-library/react';
import mockAxios from 'axios';
import Hello from '.';
afterEach(cleanup);
it('renders hello correctly', async () => {
mockAxios.get.mockResolvedValue({
data: [
{ id: 1, title: 'post one' },
{ id: 2, title: 'post two' },
],
});
const { asFragment } = await waitForElement(() => render(<Hello />));
expect(asFragment()).toMatchSnapshot();
});
6 ответов
Использовать waitForElement
правильный способ, из документов:
Подождите, пока насмехается
get
Обещание запроса разрешается, и компонент вызывает setState и повторно выполняет рендеринг.waitForElement
ждет, пока обратный вызов не выдаст ошибку
Вот рабочий пример для вашего случая:
index.jsx
:
import React, { useState, useEffect } from 'react';
import axios from 'axios';
export default function Hello() {
const [posts, setPosts] = useState([]);
useEffect(() => {
const fetchData = async () => {
const response = await axios.get('https://jsonplaceholder.typicode.com/posts');
setPosts(response.data);
};
fetchData();
}, []);
return (
<div>
<ul data-testid="list">
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
);
}
index.test.jsx
:
import React from 'react';
import { render, cleanup, waitForElement } from '@testing-library/react';
import axios from 'axios';
import Hello from '.';
jest.mock('axios');
afterEach(cleanup);
it('renders hello correctly', async () => {
axios.get.mockResolvedValue({
data: [
{ id: 1, title: 'post one' },
{ id: 2, title: 'post two' },
],
});
const { getByTestId, asFragment } = render(<Hello />);
const listNode = await waitForElement(() => getByTestId('list'));
expect(listNode.children).toHaveLength(2);
expect(asFragment()).toMatchSnapshot();
});
Результаты модульного тестирования со 100% покрытием:
PASS stackru/60115885/index.test.jsx
✓ renders hello correctly (49ms)
-----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-----------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
index.jsx | 100 | 100 | 100 | 100 |
-----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 1 passed, 1 total
Time: 4.98s
index.test.jsx.snapshot
:
// Jest Snapshot v1
exports[`renders hello correctly 1`] = `
<DocumentFragment>
<div>
<ul
data-testid="list"
>
<li>
post one
</li>
<li>
post two
</li>
</ul>
</div>
</DocumentFragment>
`;
исходный код: https://github.com/mrdulin/react-apollo-graphql-starter-kit/tree/master/stackru/60115885
у меня была ошибка:
console.error
Warning: A suspended resource finished loading inside a test, but the event was not wrapped in act(...).
When testing, code that resolves suspended data should be wrapped into act(...):
act(() => {
/* finish loading suspended data */
});
/* assert on the output */
This ensures that you're testing the behavior the user would see in the browser. Learn more at https://reactjs.org/link/wrap-tests-with-act
код:
test('check login link', async () => {
renderRouter({ initialRoute: [home.path] });
const loginLink = screen.getByTestId(dataTestIds.loginLink);
expect(loginLink).toBeInTheDocument();
userEvent.click(loginLink);
const emailInput = screen.getByTestId(dataTestIds.emailInput);
expect(emailInput).toBeInTheDocument();
}
я решил как:
test('check login link', async () => {
renderRouter({ initialRoute: [home.path] });
const loginLink = screen.getByTestId(dataTestIds.loginLink);
expect(loginLink).toBeInTheDocument();
userEvent.click(loginLink);
await waitFor(() => {
const emailInput = screen.getByTestId(dataTestIds.emailInput);
expect(emailInput).toBeInTheDocument();
});
}
я только что завернул в обратный вызов fn - waitFor()
Может кому будет полезно
WaitFor
сработало для меня, я пытался использоватьfindByTestId
как упоминалось здесь, но я все еще получаю ту же ошибку действия.
Мое решение:
it('Should show an error message when pressing “Next” with no email', async () => {
const { getByTestId, getByText } = render(
<Layout buttonText={'Common:Actions.Next'} onValidation={() => validationMock}
/>
);
const validationMock: ValidationResults = {
email: {
state: ValidationState.ERROR,
message: 'Email field cannot be empty'
}
};
await waitFor(() => {
const nextButton = getByTestId('btn-next');
fireEvent.press(nextButton);
});
expect(getByText('Email field cannot be empty')).toBeDefined();
Для меня решением было дождаться
waitForNextUpdate
it('useMyHook test', async() => {
const {
result,
waitForNextUpdate
} = renderHook(() =>
useMyHook(),
);
await waitForNextUpdate()
expect(result.current).toEqual([])
}
Ответ slideshowp2 выше хорош, но довольно специфичен для вашего конкретного примера. (Его ответ, кажется, не работает, потому что он не ждет разрешения обещания axios; всегда естьtestid
присутствует, но это легко исправить.)
Если ваш код изменится так, что, например, послеlist
testId
найден, выполняется утверждение, затем другоеuseEffect
срабатывает, что вызывает обновления состояния, о которых вам все равно, вы снова получите ту же проблему. Общее решение состоит в том, чтобы обернуть in, чтобы убедиться, что все обновления выполнены, прежде чем переходить к утверждениям и окончанию теста. Кроме того, эти утверждения не должныwaitFor
что-либо. Перепишите тело теста следующим образом:
axios.get.mockResolvedValue({
data: [
{ id: 1, title: 'post one' },
{ id: 2, title: 'post two' },
],
});
let getByTestId;
let asFragment;
await act(()=>{
const component = render(<Hello />);
getByTestId = component.getByTestId;
asFragment = component.asFragment;
});
const listNode = getByTestId('list');
expect(listNode.children).toHaveLength(2);
expect(asFragment()).toMatchSnapshot();
(Импорт из библиотеки тестирования.)
Обратите внимание, чтоrender
завернут вact
, а поиск списка выполняется с помощьюgetBy*
который не является асинхронным! Все обещания резолюции завершены доgetByTestId
call, поэтому после окончания теста не происходит никаких обновлений состояния.
Распространенным источником этой ошибки, в частности, является то, что код, запускающий изменение состояния, неwaiting
чтобы произошли изменения в состоянии. Кажется очевидным, да? но, возможно, это не так очевидно при запуске такого большого количества обновлений состояния. Позвольте мне проиллюстрировать мой случай:
// Assert
const user = userEvent.setup();
render(<RouterProvider router={router} />);
// Get a reference to the input elements on the form
const titleField = screen.getByRole("textbox", { name: /title/i });
const descriptionField = screen.getByRole("textbox", {
name: /description/i,
});
// Get a reference to the submit button
const saveButton = screen.getByRole("button", { name: /Save/i });
// Act
// Fill the inputs
user.type(titleField, "Go to party");
user.type(descriptionField, "Let's have some fun!");
// Click on save todo
await user.click(saveButton);
// Assert
// expect sentences
Все выглядит так прекрасно, правда? Но.type
также является асинхронным методом и запускает обновление состояния. РЕШЕНИЕ
// Act
// Fill the inputs (and wait for the state to update)
await user.type(titleField, "Go to party");
await user.type(descriptionField, "Let's have some fun!");
// Click on save todo
await user.click(saveButton);
// Assert
// expect sentences