如何解决测试-库-反应中的“更新没有包装在 act()中”警告?

我正在使用一个有副作用的简单组件。我的测试通过了,但是我得到了 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();
});
122113 次浏览

Updated answer:

Please refer to @mikaelrs comment below.

No need for the waitFor or waitForElement. You can just use findBy* selectors which return a promise that can be awaited. e.g await findByTestId('list');


Deprecated answer:

Use waitForElement is a correct way, from the docs:

Wait until the mocked get request promise resolves and the component calls setState and re-renders. waitForElement waits until the callback doesn't throw an error

Here is the working example for your case:

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();
});

Unit test results with 100% coverage:

 PASS  stackoverflow/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>
`;

source code: https://github.com/mrdulin/react-apollo-graphql-starter-kit/tree/master/stackoverflow/60115885

For me, the solution was to wait for waitForNextUpdate

it('useMyHook test', async() => {
const {
result,
waitForNextUpdate
} = renderHook(() =>
useMyHook(),
);
await waitForNextUpdate()
expect(result.current).toEqual([])
}

i had a error:

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

code:

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();
}

i have resolved like:

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();
});
}

i have just wrapped in callback fn - waitFor()

Maybe will be useful for someone

slideshowp2's answer above is good but pretty specific to your particular example. (His answer doesn't seem to work because it doesn't wait for the axios promise to resolve; there is always a list testid present, but that is easily fixed.)

If your code changes so that, for instance, after the list testId is found, the asserts run, then another useEffect is triggered which causes state updates about which you don't care, you'll get the same act problem again. A general solution is to wrap the render in act to make sure all updates are done before proceeding with assertions and the end of the test. Also, those assertions won't need to waitFor anything. Rewrite the test body as follows:

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();

(Import act from the testing library.)

Note that render is wrapped in act, and finding the list is done using getBy* which is not asynchronous! All the promise resolutions are complete before the getByTestId call, so no state updates happen after the end of the test.

WaitFor worked for me, I've tried using findByTestId as mentioned here, but I still got the same action error.

My solution:

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();

This works for me.

    import React from 'react';
import { render, waitFor } from '@testing-library/react';
import App from './App';
import { Loader } from './components';
    

describe('App', () => {
test('renders App component', () => {
render(<React.Suspense fallback={<Loader open={true} backgroundColor="black" />}><App /></React.Suspense>)
const el = document.querySelector('app')
waitFor(() => { expect(sel).toBeInTheDocument() })
        

});
});