Jest中toBeCalledWith匹配函数传入的JSX.Element不匹配问题
问题:Mock React组件函数时匹配JSX.Element失败
背景
我们有一个内部UI库,其中的toast函数负责渲染传入的JSX.Element。原本用React Testing Library做测试,但该库存在bug(UI运行正常,已提交维护团队修复),等待修复期间我mock了这个函数,但匹配预期JSX.Element时始终失败。
React组件代码
import { toast } from '@company/alerts/toast' export const ToastTest = ({title}: {title: string}) => { toast(<h1> {title} </h1>) return <div testId='toast-location-div'></div> }
初始测试代码
import React from 'react' import { ToastTest } from './toast-test' import { toast } from '@company/alerts/toast' import { render, waitFor } from 'react-testing-library' jest.mock('@company/alerts', () => { ...jest.requireActual('@company/alerts'), toast: jest.fn() }) const TestTitleComponent = ({title}) => <h1> {title} </h1> describe('Toast test', () => { it('should call toast', async () => { render(<ToastTest title="foo" />) await waitFor(() => { expect(toast).toHaveBeenCalledWith(<TestTitleComponent title="foo" />) }) }) })
初始报错信息
expect(jest.fn()).toHaveBeenCalledWith(...expected) - Expected + Received - <h1> foo </h1> + <h1> foo </h1>
看起来内容完全一致,但实际是React元素的内部引用/属性不同,导致匹配失败。
后续尝试的问题
按照建议改用spy获取调用参数,尝试多种匹配方式均失败:
- 使用
toStrictEqual匹配完整元素:
expect(toastSpy.mock.calls[0][0]).toStrictEqual( <h1> foo </h1> )
报错:
Expected: <h1> foo </h1> Received: serializes to the same string
- 使用
expect.objectContaining匹配元素结构:
await waitFor(() => { expect(toastSpy).toHaveBeenCalled() expect(expect.objectContaining(toastSpy.mock.calls[0][0])).toEqual( expect.objectContaining( <h1> foo </h1> ) ) })
报错显示元素内部属性(如_owner)差异过大,无法匹配。
3. 尝试直接访问props时遇到TypeScript错误:
expect(toastSpy.mock.calls[0][0]['props']).toEqual({ title: 'foo' })
报错:
src/banner/components/banner-toast.test.tsx:39:40 - error TS2304: Cannot find name 'props'.
最后用Object.values()取第4位的props勉强成功,但这种方法非常不优雅。
解决方案
方案1:类型断言解决TS问题,直接匹配props
不用hack式的索引,给React元素做类型断言,直接访问props:
await waitFor(() => { expect(toastSpy).toHaveBeenCalled(); const toastArg = toastSpy.mock.calls[0][0] as React.ReactElement<{title: string}>; expect(toastArg.props).toEqual({ title: 'foo' }); });
既解决了TypeScript的类型报错,又能精准匹配关键的props内容,避免因React元素内部属性差异导致的匹配失败。
方案2:拆解元素关键属性验证
在断言时分别验证元素的类型和内容,不依赖完整元素匹配:
it('should call toast with correct title', async () => { render(<ToastTest title="foo" />); await waitFor(() => { expect(toast).toHaveBeenCalled(); const [receivedElement] = toast.mock.calls[0]; // 验证元素类型是h1 expect(receivedElement.type).toBe('h1'); // 验证子内容包含目标文本 expect(receivedElement.props.children).toContain('foo'); }); });
这种方式更灵活,能同时确认元素类型和内容是否符合预期。
方案3:调整mock逻辑,用RTL查询DOM元素
如果可以修改mock让toast把元素渲染到测试容器里,就可以用RTL的查询方式验证,更贴近用户视角:
// 修改mock,让toast把元素挂载到组件返回的容器中 jest.mock('@company/alerts', () => ({ ...jest.requireActual('@company/alerts'), toast: (element: React.ReactElement) => { const container = document.getElementById('toast-location-div'); if (container) { ReactDOM.render(element, container); } } })); it('should render toast with correct title', async () => { render(<ToastTest title="foo" />); await waitFor(() => { expect(screen.getByText('foo')).toBeInTheDocument(); }); });
这种方式符合RTL的测试理念,关注用户能看到的实际内容,而非内部函数调用细节。
内容的提问来源于stack exchange,提问作者LucyEly
相关产品推荐
相关产品推荐

