Как устранить предупреждение "обновление не было обернуто в 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*который не является асинхронным! Все обещания резолюции завершены доgetByTestIdcall, поэтому после окончания теста не происходит никаких обновлений состояния.

Распространенным источником этой ошибки, в частности, является то, что код, запускающий изменение состояния, не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
Другие вопросы по тегам