You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何测试模拟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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.16 06:10:58