使用jest-fetch-mock模拟多API时如何消除act()包裹警告?
问题背景
我在为React应用编写单元测试时遇到了麻烦:组件通过自定义hook useFetchReferenceData 调用14个静态API接口来填充各类下拉选项,测试时用jest-fetch-mock模拟了所有接口响应,测试能通过,但控制台输出大量如下警告:
Warning: An update to SearchSubmitterProfile inside a test was not wrapped in act(...).
When testing, code that causes React state updates should be wrapped into act(...):
act(() => {
/* fire events that update state /
});
/ assert on the output */
核心代码片段
组件代码
export function SearchSubmitterProfile() { const [search, setSearch] = useState(true); const [viewResults, setViewResults] = useState(false); const [createSubmitterProfileState, setCreateSubmitterProfileState] = useState(INITIAL_STATE); useFetchReferenceData( createSubmitterProfileState, setCreateSubmitterProfileState ); return ( <div id="search-sub-profile" key="search-sub-profile" className={styles.grayBg}> {search ? <ConnectedSearchView /> : null} {viewResults ? <ConnectedSubmitterProfile /> : null} </div> ); } export default SearchSubmitterProfile;
自定义hook核心逻辑
const { run: getLanguageIndicators } = useFetchye(`${subApiUrl}/submlanguageind`, { credentials: 'include', headers: getDefaultHeaders(), defer: true, }); const buildLanguagIndDropdown = async () => { const currentState = { ...createSubmitterProfileState }; const { data } = await getLanguageIndicators(); if (data.status === 200) { const result = [DROPDOWN_STARTER]; data.body.forEach(element => result.push({ value: `${element.dialect_language_code}`, label: `${element.dialect_language_code} - ${element.dialect_name}`, })); currentState.staticFieldState.submitterLanguageIndicator.options = result; setCreateSubmitterProfileState(currentState); } else { errorList.push('Error retrieving language indicators'); } }; useEffect(() => { const fetchAllData = async () => { await buildRegionsDropDown(); await buildLanguagIndDropdown(); // ... 另外14个类似的构建函数 }; fetchAllData().catch(console.error); }, []);
测试代码
describe('Search Submitter Profile reference data', () => { beforeEach(() => { enableFetchMocks(); fetch.mockResponses( [JSON.stringify(mockRegions), { status: 200 }], [JSON.stringify(mockLanguageIndicators), { status: 200 }], // ... 其他14个模拟响应 ); }); it('validate the search information view', async () => { render( <UserContext.Provider value={{ loggedInUser }}> <SearchSubmitterProfile /> </UserContext.Provider> ); }); });
解决方法
1. 用waitFor等待异步状态更新
React Testing Library的waitFor会自动把异步状态更新包裹在act(...)里,确保所有更新完成后再结束测试。修改测试代码:
import { render, waitFor, screen } from '@testing-library/react'; describe('Search Submitter Profile reference data', () => { beforeEach(() => { enableFetchMocks(); fetch.mockResponses( [JSON.stringify(mockRegions), { status: 200 }], [JSON.stringify(mockLanguageIndicators), { status: 200 }], // ... 其他14个模拟响应 ); }); it('validate the search information view', async () => { render( <UserContext.Provider value={{ loggedInUser }}> <SearchSubmitterProfile /> </UserContext.Provider> ); // 等待所有API请求和状态更新完成 await waitFor(() => { // 这里可以加具体断言,比如验证某个下拉选项是否加载成功 // 示例:expect(screen.getByText('zh-CN - 中文')).toBeInTheDocument(); }); }); });
2. 修复useEffect的依赖数组(可选优化)
原hook的useEffect依赖数组为空,但实际依赖createSubmitterProfileState和setCreateSubmitterProfileState,补上依赖可以避免闭包问题,让测试更稳定:
useEffect(() => { const fetchAllData = async () => { await buildRegionsDropDown(); await buildLanguagIndDropdown(); // ... 其他构建函数 }; fetchAllData().catch(console.error); }, [createSubmitterProfileState, setCreateSubmitterProfileState]); // 添加缺失的依赖
3. 用findBy*查询方法断言异步内容
如果需要验证异步加载的UI元素,直接用findBy*系列方法(内部整合了waitFor和getBy*),自动等待状态更新:
it('validate the search information view', async () => { render( <UserContext.Provider value={{ loggedInUser }}> <SearchSubmitterProfile /> </UserContext.Provider> ); // 等待下拉框加载并断言选项存在 const languageOption = await screen.findByText('zh-CN - 中文'); expect(languageOption).toBeInTheDocument(); });
原因说明
警告的本质是:测试代码执行完毕后,异步API请求才完成并触发了组件状态更新,这些更新没有被React的act(...)包裹。waitFor和findBy*会自动处理act(...)的包裹逻辑,确保所有状态更新完成后再执行断言,彻底消除警告。
内容的提问来源于stack exchange,提问作者VoodooJellyfish

