如何在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
相关产品推荐
相关产品推荐

