Storybook+Jest play方法调用shouldHaveBeenCalled断言失败
Pagination组件Storybook交互测试失败问题
组件代码
import { FC, ReactElement, useState, MouseEvent as ReactMouseEvent, ChangeEvent as ReactChangeEvent, } from 'react'; import { Stack, TablePagination } from '@mui/material'; export const Pagination: FC<IPaginationProps> = ({ total, page = 1, rowsPerPage = 25, position = 'start', rowsPerPageOptions = [10, 25, 50, 100], onPageChange, onRowsPerPageChange, }): ReactElement => { const [localPage, setPage] = useState(page); const [localRowsPerPage, setRowsPerPage] = useState(rowsPerPage); const handleChangePage = (event: ReactMouseEvent<HTMLButtonElement> | null, newPage: number) => { setPage(newPage); onPageChange({ page: newPage, total, rowsPerPage: localRowsPerPage }); }; const handleChangeRowsPerPage = ( event: ReactChangeEvent<HTMLInputElement | HTMLTextAreaElement>, ) => { const perPage = parseInt(event.target.value, 10); setRowsPerPage(perPage); setPage(0); onRowsPerPageChange({ page: 0, total, rowsPerPage: perPage }); }; return ( <Stack direction="row" justifyContent={`flex-${position}`}> <TablePagination component="div" showFirstButton showLastButton count={total} page={localPage} onPageChange={handleChangePage} rowsPerPage={localRowsPerPage} onRowsPerPageChange={handleChangeRowsPerPage} rowsPerPageOptions={rowsPerPageOptions} /> </Stack> ); };
Storybook故事代码
export default { title: 'Components/Pagination', component: Pagination, parameters: { actions: { handles: ['click'], }, }, args: { total: 100, page: 1, onPageChange: ({ page }) => { // eslint-disable-next-line no-console console.log({ page }); }, onRowsPerPageChange: ({ rowsPerPage }) => { // eslint-disable-next-line no-console console.log({ rowsPerPage }); }, }, decorators: [ (Story) => ( <div style={{ margin: '3em', boxShadow: '7px 5px 14px -8px rgba(0,0,0,0.55)' }}> {Story()} </div> ), ], } as ComponentMeta<typeof Pagination>; const Template: ComponentStory<typeof Pagination> = ({ ...args }) => <Pagination {...args} />; export const Default = Template.bind({}); Default.play = async ({ canvasElement, args }) => { const onPageChangeSpy = jest.spyOn(args, 'onPageChange'); const canvas = await within(canvasElement); const prevBtn = canvas.getByLabelText('Go to previous page'); await userEvent.click(prevBtn); await expect(onPageChangeSpy).toHaveBeenCalled(); };
问题描述
按钮点击后分页状态正常变化,说明onPageChange已被调用,但测试报错:
Expected number of calls: >= 1
Received number of calls: 0
奇怪的是,在Storybook交互标签页点击重新运行后,测试又能正常通过。
问题原因
测试失败的核心原因是组件渲染时机与spy创建时机不匹配:
- Storybook会先渲染组件,再执行
play函数里的代码 - 当你在
play里用jest.spyOn给args.onPageChange创建spy时,组件已经使用原始的onPageChange函数完成渲染了 - 后续组件内部调用的还是原始函数,spy无法捕获到调用
- 重新运行时,组件会在spy创建之后重新渲染,因此能正确捕获调用
解决方案
方案1:修改play函数,先替换spy再重新渲染
Default.play = async ({ canvasElement, args, rerender }) => { // 创建spy并替换args中的原始函数 const onPageChangeSpy = jest.fn(); args.onPageChange = onPageChangeSpy; // 强制重新渲染组件,让组件使用新的spy函数 await rerender(); const canvas = within(canvasElement); const prevBtn = canvas.getByLabelText('Go to previous page'); await userEvent.click(prevBtn); await expect(onPageChangeSpy).toHaveBeenCalled(); };
方案2:直接在args中定义spy(推荐)
在Story的args里直接使用jest.fn(),让组件从一开始就使用spy函数:
args: { total: 100, page: 1, onPageChange: jest.fn(({ page }) => { console.log({ page }); }), onRowsPerPageChange: jest.fn(({ rowsPerPage }) => { console.log({ rowsPerPage }); }), },
然后修改play函数直接引用args中的spy:
Default.play = async ({ canvasElement, args }) => { const canvas = within(canvasElement); const prevBtn = canvas.getByLabelText('Go to previous page'); await userEvent.click(prevBtn); await expect(args.onPageChange).toHaveBeenCalled(); };
两种方案都能确保组件使用的是spy函数,测试即可正常捕获调用。
内容的提问来源于stack exchange,提问作者Tomer
相关产品推荐
相关产品推荐

