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

如何在React Testing Library测试中引入useMemo动态值?

问题描述

我开发了一个带日期单选按钮的StockReportModal组件,供用户下载报告时选择日期。日期通过useMemo函数根据当前年度动态生成并按从旧到新排序,默认选中数组第0项。使用React Testing Library编写测试时,无法在测试文件中获取该useMemo生成的动态默认值,尝试在测试中使用useMemo触发了Hooks错误,现寻求解决方案。

组件及测试代码如下:

组件代码

const StockReportModal = (props) => {
    const { t } = useTranslation();
    const { close, onDownloadStockReport } = props;

    const [baseDate, setBaseDate] = useState("");

    const datesArrayFormatted = useMemo(() => {
        const year = new Date().getFullYear();
        let datesArray = [new Date(year, 2, 31), new Date(year, 5, 30), new Date(year, 8, 30), new Date(year, 11, 31)];

        datesArray.forEach((date) => {
            if (date > new Date()) {
                date.setFullYear(year - 1);
            }
        });

        datesArray.sort(function (a, b) {
            return a - b;
        });

        return datesArray;
    }, []);

    useEffect(() => {
        let date;
        date = formatUTCEndDate(datesArrayFormatted[0]);
        setBaseDate({ date: moment(date).toISOString() });
    }, [datesArrayFormatted]);

    const DateOptions = useMemo(
        () => ({
            first: "first",
            second: "second",
            third: "third",
            fourth: "fourth",
            custom: "custom",
        }),
        []
    );

    const dateOptions = useMemo(() => {
        return [
            {
                value: DateOptions.first,
                description: moment(datesArrayFormatted[0]).format("MMMM Do YYYY"),
            },
            {
                value: DateOptions.second,
                description: moment(datesArrayFormatted[1]).format("MMMM Do YYYY"),
            },
            {
                value: DateOptions.third,
                description: moment(datesArrayFormatted[2]).format("MMMM Do YYYY"),
            },
            {
                value: DateOptions.fourth,
                description: moment(datesArrayFormatted[3]).format("MMMM Do YYYY"),
            },
            { value: DateOptions.custom, description: "Custom" }
        ];
    }, [DateOptions, t, datesArrayFormatted]);

    const onDateChange = useCallback(
        (val) => {
            let date;
            switch (val) {
                case DateOptions.first:
                    date = formatUTCEndDate(datesArrayFormatted[0]);
                    setBaseDate({ date: moment(date).toISOString() });
                    break;
                case DateOptions.second:
                    date = formatUTCEndDate(datesArrayFormatted[1]);
                    setBaseDate({ date: moment(date).toISOString() });
                    break;
                case DateOptions.third:
                    date = formatUTCEndDate(datesArrayFormatted[2]);
                    setBaseDate({ date: moment(date).toISOString() });
                    break;
                case DateOptions.fourth:
                    date = formatUTCEndDate(datesArrayFormatted[3]);
                    setBaseDate({ date: moment(date).toISOString() });
                    break;
                case DateOptions.custom:
                    break;
                default:
                    break;
            }
            setDateValue(val);
        },
        [DateOptions, setBaseDate, setDateValue, datesArrayFormatted]
    );

    const onFileChange = useCallback(
        (val) => {
            setFileValue(val);
        },
        [setFileValue]
    );

    const onDownloadReport = useCallback(() => {
        onDownloadStockReport({
            dates: { date: baseDate.date },
            file_type: fileValue,
            service: ReportTypes.stocks,
        });
        close();
    }, [close, baseDate, fileValue, onDownloadStockReport]);

    return (
        <Modal close={close} className="report-stock-modal">
            <ModalBody>
                <Heading align="center" level="2">
                    Reports
                </Heading>
                <Radio options={dateOptions} testId="stock-date-option" value={dateValue} onChange={onDateChange} />
            </ModalBody>
        </Modal>
    );
};

export default StockReportModal;

测试代码

describe("StockReportModal test", () => {
    const defaultDate = formatUTCEndDate(moment(datesArrayFormatted[0]).format("MMMM Do YYYY").toISOString());
    const defaultFileType = "pdf";

    const sampleDate = "20/01/2020";
    it("should render with initial state", () => {
        render(<StockReportModal {...props} />);
        const downloadBtn = screen.getByTestId("stock-save");
        fireEvent.click(downloadBtn);

        expect(props.onDownloadStockReport).toHaveBeenCalledTimes(1);
        expect(props.onDownloadStockReport).toHaveBeenCalledWith({
            dates: { date: defaultDate },
            file_type: defaultFileType,
            service: ReportTypes.stocks,
        });

        expect(props.close).toHaveBeenCalledTimes(1);
    });
});

解决方案

方案1:提取日期生成逻辑为独立工具函数

把组件内useMemo中的日期生成逻辑抽成纯函数,这样组件和测试都能直接调用,既避免重复代码,也不会触发Hooks规则错误。

步骤1:创建工具函数

// utils/dateUtils.js
export const generateDatesArray = () => {
  const year = new Date().getFullYear();
  let datesArray = [new Date(year, 2, 31), new Date(year, 5, 30), new Date(year, 8, 30), new Date(year, 11, 31)];

  datesArray.forEach((date) => {
    if (date > new Date()) {
      date.setFullYear(year - 1);
    }
  });

  datesArray.sort((a, b) => a - b);

  return datesArray;
};

步骤2:修改组件使用该函数

import { generateDatesArray } from './utils/dateUtils';

const StockReportModal = (props) => {
  // ... 其他代码不变
  const datesArrayFormatted = useMemo(() => generateDatesArray(), []);
  // ... 其他代码不变
};

步骤3:测试文件中调用函数获取默认值

import { generateDatesArray } from './utils/dateUtils';

describe("StockReportModal test", () => {
  const datesArray = generateDatesArray();
  const defaultDate = formatUTCEndDate(moment(datesArray[0]).toISOString());
  const defaultFileType = "pdf";

  it("should render with initial state", () => {
    render(<StockReportModal {...props} />);
    const downloadBtn = screen.getByTestId("stock-save");
    fireEvent.click(downloadBtn);

    expect(props.onDownloadStockReport).toHaveBeenCalledTimes(1);
    expect(props.onDownloadStockReport).toHaveBeenCalledWith({
      dates: { date: defaultDate },
      file_type: defaultFileType,
      service: ReportTypes.stocks,
    });

    expect(props.close).toHaveBeenCalledTimes(1);
  });
});

方案2:测试组件行为而非硬编码预期值

不需要提前计算默认日期,而是通过捕获组件的调用参数,验证其结构和逻辑是否符合预期,这样更贴近用户实际使用场景,也避免因时间变化导致测试不稳定。

修改后的测试代码

describe("StockReportModal test", () => {
  const defaultFileType = "pdf";

  it("should render with initial state and use default date", () => {
    render(<StockReportModal {...props} />);
    const downloadBtn = screen.getByTestId("stock-save");
    fireEvent.click(downloadBtn);

    // 获取onDownloadStockReport的调用参数
    const [callParams] = props.onDownloadStockReport.mock.calls[0];
    
    expect(props.onDownloadStockReport).toHaveBeenCalledTimes(1);
    // 验证参数结构正确
    expect(callParams).toMatchObject({
      file_type: defaultFileType,
      service: ReportTypes.stocks,
    });
    // 验证日期是有效的季度末日期
    const date = moment(callParams.dates.date);
    expect(date.isValid()).toBe(true);
    // 季度末对应的月份(0为1月,所以2是3月、5是6月等)
    const quarterEndMonths = [2, 5, 8, 11];
    expect(quarterEndMonths.includes(date.month())).toBe(true);
  });
});

可选:固定测试中的日期

为了避免当前系统时间变化导致测试结果不稳定,可以用Jest模拟系统时间:

describe("StockReportModal test", () => {
  beforeAll(() => {
    // 模拟当前日期为2024年6月1日,此时6月30日尚未到来,会被替换为2023年的日期
    jest.useFakeTimers().setSystemTime(new Date('2024-06-01'));
  });

  afterAll(() => {
    // 恢复真实时间
    jest.useRealTimers();
  });

  it("should render with initial state", () => {
    // ... 测试逻辑
  });
});

内容的提问来源于stack exchange,提问作者Asher

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.21 16:58:58