如何测试模拟Fetch API的React组件?渲染异常求助
测试含Fetch调用的React组件问题
组件代码
class SearchResults extends React.Component<{ tag: string; setSpinner: (value: boolean) => void }> { state: searchResultsState = { results: [], tag: '', cardFull: null, }; constructor(props: { tag: string; setSpinner: (value: boolean) => void }) { super(props); this.handleCardModal = this.handleCardModal.bind(this); } componentDidMount() { getResponse( this.props.tag, (res) => { this.setState({ results: res, tag: this.props.tag }); }, this.props.setSpinner ); } componentDidUpdate(prevProps: { tag: string }) { if (this.props.tag !== prevProps.tag) { getResponse( this.props.tag, (res) => this.setState({ results: res, tag: this.props.tag }), this.props.setSpinner ); } } handleCardModal() { this.setState({ cardFull: null }); } render() { return ( <Fragment> {this.state.cardFull && ( <CardFull data={this.state.cardFull} handleClick={this.handleCardModal} /> )} {this.state.cardFull && <div className="blackout" onClick={this.handleCardModal} />} <div className="search-results"> {this.state.results.length === 0 && this.state.tag !== '' && <div>Sorry, no matched</div>} {this.state.results.map((result) => ( <CardUI key={result.id} className="card" variant="outlined" sx={{ minWidth: 275 }} onClick={() => { const currentCard = this.state.results.filter((res) => res.id === result.id)[0]; this.setState({ ...this.state, cardFull: currentCard }); }} > <CardMedia component="img" height="194" image={getUrl(result)} alt={result.title} /> <CardContent> <p>{result.title}</p> </CardContent> </CardUI> ))} </div> </Fragment> ); } }
最初的测试代码(使用jest-fetch-mock)
import '@testing-library/jest-dom'; import { render } from '@testing-library/react'; import renderer from 'react-test-renderer'; import SearchResults from '../../src/components/SearchResults/SearchResults'; import sampleSearchResults from '../__fixtures__/sampleSearchResults'; import fetch from 'jest-fetch-mock'; fetch.enableMocks(); beforeEach(() => { fetch.resetMocks(); }); const setSpinner = jest.fn(); describe('Search Results component', () => { fetch.mockResponseOnce(JSON.stringify({ photos: sampleSearchResults })); test('Search Results matches snapshot', () => { const searchResults = renderer .create(<SearchResults tag={''} setSpinner={setSpinner} />) .toJSON(); expect(searchResults).toMatchSnapshot(); }); test('search results renders correctly', () => { render(<SearchResults setSpinner={setSpinner} tag={'dove'} />); }); });
遇到的错误
console.error FetchError { message: 'invalid json response body at reason: Unexpected end of JSON input', type: 'invalid-json' }
调整后的测试代码(手动Mock Fetch)
import React from 'react'; import '@testing-library/jest-dom'; import { render, screen } from '@testing-library/react'; import renderer from 'react-test-renderer'; import SearchResults from '../../src/components/SearchResults/SearchResults'; import sampleSearchResults from '../__fixtures__/sampleSearchResults'; const setSpinner = jest.fn(); global.fetch = jest.fn(() => Promise.resolve({ json: () => Promise.resolve({ photos: sampleSearchResults }), }) ) as jest.Mock; describe('Search Results component', () => { test('Search Results matches snapshot', () => { const searchResults = renderer .create(<SearchResults tag={''} setSpinner={setSpinner} />) .toJSON(); expect(searchResults).toMatchSnapshot(); }); test('search results renders correctly', () => { render(<SearchResults setSpinner={setSpinner} tag={'dove'} />); const title = screen.getByText(/Eurasian Collared Dove (Streptopelia decaocto)/i); expect(title).toBeInTheDocument(); //mistake }); });
当前问题
Fetch Mock已正常工作,但组件仅渲染search-results的div,无法渲染卡片,求正确测试方法。
解决方案
1. 核心问题:未等待异步操作完成
组件在componentDidMount/componentDidUpdate中调用的getResponse是异步操作,测试时直接调用render或renderer.create会立即获取初始状态(空results),此时卡片还未渲染。需要等待异步数据加载完成、状态更新后再进行断言。
2. 修正测试代码示例
import React from 'react'; import '@testing-library/jest-dom'; import { render, screen, waitFor } from '@testing-library/react'; import renderer from 'react-test-renderer'; import SearchResults from '../../src/components/SearchResults/SearchResults'; import sampleSearchResults from '../__fixtures__/sampleSearchResults'; const setSpinner = jest.fn(); describe('Search Results component', () => { // 每次测试前重置fetch Mock,避免测试间干扰 beforeEach(() => { global.fetch = jest.fn(() => Promise.resolve({ json: () => Promise.resolve({ photos: sampleSearchResults }), }) ) as jest.Mock; }); test('生成数据加载后的快照', async () => { const component = renderer.create(<SearchResults tag={'dove'} setSpinner={setSpinner} />); // 等待异步数据加载完成并更新组件状态 await waitFor(() => { component.update(); }); const searchResults = component.toJSON(); expect(searchResults).toMatchSnapshot(); }); test('正确渲染搜索结果卡片', async () => { render(<SearchResults setSpinner={setSpinner} tag={'dove'} />); // 使用findByText等待异步渲染的元素出现(findBy是waitFor+getBy的组合) const title = await screen.findByText(/Eurasian Collared Dove \(Streptopelia decaocto\)/i); expect(title).toBeInTheDocument(); // 验证卡片数量是否与测试数据一致 const cards = await screen.findAllByClassName('card'); expect(cards).toHaveLength(sampleSearchResults.length); }); test('无结果时显示提示文本', async () => { // Mock空结果的响应 global.fetch = jest.fn(() => Promise.resolve({ json: () => Promise.resolve({ photos: [] }), }) ) as jest.Mock; render(<SearchResults setSpinner={setSpinner} tag={'nonexistent'} />); const noMatchText = await screen.findByText('Sorry, no matched'); expect(noMatchText).toBeInTheDocument(); }); });
3. 额外检查点
- 确认
getResponse的数据处理逻辑:组件中getResponse的回调直接把res赋值给results,需要确保getResponse内部是将接口返回的photos数组传递给回调。如果getResponse直接传递整个响应对象(即res是{photos: [...]}),那么组件的results会是一个对象而非数组,map方法不会执行,自然不会渲染卡片。此时需要调整Mock返回为Promise.resolve(sampleSearchResults)。 - Mock重置:必须在
beforeEach中重置global.fetch,防止不同测试用例的Mock状态互相污染。
内容的提问来源于stack exchange,提问作者Oksana
相关产品推荐
相关产品推荐

