使用Chart.js的React组件Jest测试报错:如何正确Mock Chart.js?
Chart.js 3.9.1 + React组件Jest测试报错:TypeError: undefined is not iterable
问题详情
React组件DataLineChart基于Chart.js 3.9.1开发,搭配自定义useUser Hook。编写Jest测试用例时,组件渲染阶段触发错误,定位到Chart.register(...registerables)行,错误信息:
TypeError: undefined is not iterable (cannot read property Symbol(symbol.iterator))
已成功Mock自定义Hook,但尝试多种Chart.js Mock方式均无效,其他组件测试正常,当前代码覆盖率95%,问题已持续10天。
组件代码
import React, { useEffect, useRef, useState } from 'react'; import { Chart, ChartConfiguration, registerables } from 'chart.js'; import './EmailLineChart.styles.css'; import { useUser } from '../../common/context/context'; const DataLineChart: React.FC = () => { const chartRef = useRef<HTMLCanvasElement>(null); const chartInstanceRef = useRef<Chart | null>(null); const [data, setData] = useState<{ month: string; success: number; failed: number; processing?: number }[]>([]); const [downloadFormat, setDownloadFormat] = useState<string>('chart'); const [filterMonth, setFilterMonth] = useState<string>(''); const [chartType, setChartType] = useState<'line'|'bar'>('line'); const baseUrl = process.env.REACT_APP_BASE_URL; // 注:原代码中tag和servicename变量未定义,需确保实际代码中有正确赋值 let url = `${baseUrl}/email/reports/count?type=year&tag=${tag}&service_name=${servicename}`; const tenantData = useUser(); useEffect(() => { const fetchData = async () => { try { const response = await fetch(url); const apiData = await response.json(); const monthlyReports = apiData.monthlyReports; if (!monthlyReports) { console.error('No monthlyReports data in API response'); return; } const chartData = Object.keys(monthlyReports).map((month) => ({ month, success: monthlyReports[month].SUCCESS, failed: monthlyReports[month].FAILED, processing: monthlyReports[month].PROCESSING || 0, })); chartData.sort((a, b) => new Date(a.month).getTime() - new Date(b.month).getTime()); setData(chartData); } catch (error) { console.error('Error fetching data:', error); } }; fetchData(); }, []); useEffect(() => { if (chartInstanceRef.current) { chartInstanceRef.current.destroy(); } if (chartRef.current && downloadFormat === 'chart') { Chart.register(...registerables); const ctx = chartRef.current.getContext('2d'); const filteredData = filterMonth ? data.filter((item) => item.month === filterMonth) : data; if (ctx) { const gradient1 = ctx.createLinearGradient(0, 0, 0, 400); gradient1.addColorStop(0, 'rgba(75, 192, 192, 0.6)'); gradient1.addColorStop(1, 'rgba(75, 192, 192, 0)'); const gradient2 = ctx.createLinearGradient(0, 0, 0, 400); gradient2.addColorStop(0, 'rgba(255, 99, 132, 0.6)'); gradient2.addColorStop(1, 'rgba(255, 99, 132, 0)'); const config: ChartConfiguration<'line'|'bar'> = { type: chartType, data: { labels: filteredData.map((item) => item.month), datasets: [ { label: 'Success', data: filteredData.map((item) => item.success), borderColor: 'rgba(75, 192,198, 0.9)', fill: true, backgroundColor:'rgba(75, 192, 192, 0.3)', pointBackgroundColor: 'rgba(75, 192, 192, 0.9)', pointBorderColor:'rgba(75, 250, 192, 0.9)', pointHoverBackgroundColor: 'rgba(75, 350, 192, 0.9)', pointHoverBorderColor: 'rgba(75, 350, 192, 0.9)', pointHoverRadius: 8, pointHoverBorderWidth: 5, tension: 0.1, }, { label: 'Failed', data: filteredData.map((item) => item.failed), borderColor: 'rgba(255, 99, 132, 0.4)', fill: true, backgroundColor: 'rgba(255, 99, 132, 0.2)', pointBackgroundColor: 'rgba(255, 99, 132, 0.3)', pointBorderColor: 'rgba(255, 99, 132, 0.9)', pointHoverBackgroundColor: 'rgba(255, 99, 132, 0.3)', pointHoverBorderColor: 'rgba(255, 99, 132, 0.3)', pointHoverRadius: 8, pointHoverBorderWidth: 5, tension: 0.1, }, ], }, options: { responsive: true, animation: { duration: 2000, easing: 'easeInOutBounce', }, scales: { x: { title: { display: true, text: 'Month', }, grid: { color: 'gray', display: false, }, ticks: { color: 'black', font: { size: 10, } }, stacked:true }, y: { title: { display: true, text: 'Number of Emails', }, ticks: { color: 'black', font: { size: 10, } }, grid: { color: 'gray', display: false, }, stacked:true, }, } } }; chartInstanceRef.current = new Chart(ctx, config); } } }, [downloadFormat, data, filterMonth]); useEffect(() => { if (chartInstanceRef.current) { const chart = chartInstanceRef.current; chart.data.labels = data.map((item) => item.month); chart.data.datasets[0].data = data.map((item) => item.success||0); chart.data.datasets[1].data = data.map((item) => item.failed||0); chart.update(); } }, [data]); const filteredData = filterMonth ? data.filter((item) => item.month === filterMonth) : data; const isChartSelected = downloadFormat === 'chart'; return ( <div className='chart'> <canvas ref={chartRef} className='chart-wrapper'></canvas> </div> ); }; export default DataLineChart;
测试代码(原错误版本)
import React from 'react'; import { render } from '@testing-library/react'; import { Chart, registerables } from 'chart.js'; import DataLineChart from './DataLineChart'; jest.mock('../../common/context/context', () => ({ useUser: jest.fn(() => ({ tenantData: { tenantId: 'mockedTenantId' } })), })); jest.mock('chart.js') describe('DataLineChart', () => { it('renders without errors', () => { render(<DataLineChart />); }); });
错误信息
TypeError: undefined is not iterable (cannot read property Symbol(symbol.iterator))
解决方案
方案1:修复Jest的Chart.js Mock
默认jest.mock('chart.js')会将所有导出替换为undefined,导致registerables为undefined,展开运算符...registerables触发错误。需手动Mock所需导出:
import React from 'react'; import { render } from '@testing-library/react'; import DataLineChart from './DataLineChart'; // Mock自定义Hook jest.mock('../../common/context/context', () => ({ useUser: jest.fn(() => ({ tenantData: { tenantId: 'mockedTenantId' } })), })); // 正确Mock Chart.js jest.mock('chart.js', () => ({ Chart: jest.fn().mockImplementation(() => ({ destroy: jest.fn(), update: jest.fn() })), registerables: [], // 必须给registerables赋值为可迭代对象(空数组即可) register: jest.fn() })); // Mock fetch请求,避免组件请求真实接口 global.fetch = jest.fn(() => Promise.resolve({ json: () => Promise.resolve({ monthlyReports: {} }) }) ); describe('DataLineChart', () => { it('renders without errors', () => { render(<DataLineChart />); }); });
方案2:组件代码优化(从根源避免重复注册)
Chart.register(...registerables)只需全局注册一次,无需在useEffect中重复执行。将注册逻辑移到组件外部:
// 组件顶部,import之后立即注册 import { Chart, ChartConfiguration, registerables } from 'chart.js'; Chart.register(...registerables); // 全局注册一次即可 const DataLineChart: React.FC = () => { // 组件原有逻辑... };
此优化不仅能避免测试时的Mock问题,还能提升实际运行时的性能,减少不必要的重复注册操作。
内容的提问来源于stack exchange,提问作者rashmi
相关产品推荐
相关产品推荐

