如何在Jest中Mock React组件的handleCurrencyToggle函数?
React组件测试:正确Mock handleCurrencyToggle函数
我有两个React组件CurrencySelector和CurrencyItem,正在用Jest编写测试用例,目标是验证点击CurrencyItem时,对应的货币对象能被添加到选中状态数组中。由于handleCurrencyToggle函数作为props传递给了CurrencyItem,我需要在Jest测试中Mock该函数,但当前的测试代码存在错误,恳请指导如何正确Mock真实的handleCurrencyToggle函数。
CurrencySelector.tsx
import { useEffect, useState } from "react"; import { currencies } from "../constants"; import { Currency } from "../types"; import CurrencyItem from "./CurrencyItem"; const CurrencySelector = () => { const initialSelected = () => { const storedCurrencies = sessionStorage.getItem("currencies"); return storedCurrencies ? JSON.parse(storedCurrencies) : []; }; const [selected, setSelected] = useState<Currency[]>(initialSelected); const handleCurrencyToggle = (currency: Currency) => { const isSelected = selected.some( (selectedCurrency) => selectedCurrency.name === currency.name ); if (isSelected) { setSelected((prevSelected) => prevSelected.filter( (selectedCurrency) => selectedCurrency.name !== currency.name ) ); } else { setSelected((prevSelected) => [...prevSelected, currency]); } }; useEffect(() => { sessionStorage.setItem("currencies", JSON.stringify(selected)); }, [selected]); return ( <div className="currency-selector"> <div className="selected-currencies currency-items"> {selected.length > 0 ? ( selected.map((currency: Currency, index: number) => ( <div data-testid="selected-currency" key={index} className="selected-currency currency" > <span>{currency.name}</span> <button className="unselect-currency" onClick={() => handleCurrencyToggle(currency)} > x </button> </div> )) ) : ( <div data-testid="no-selected-currencies" className="no-currencies"> No selected currencies </div> )} </div> <div className="currency-items"> {currencies.map((currency: Currency, index: number) => ( <CurrencyItem data-testid={`currency-item-${index}`} key={index} currency={currency} onClick={() => handleCurrencyToggle(currency)} isSelected={selected.some( (selectedCurrency) => selectedCurrency.name === currency.name )} /> ))} </div> </div> ); }; export default CurrencySelector;
CurrencyItem.tsx
import { Currency } from "../types"; const CurrencyItem = ({ currency, onClick, isSelected, }: { currency: Currency; onClick: () => void; isSelected: boolean; }) => { const handleCheckboxChange = () => { onClick(); }; return ( <div data-testid="currency-item" onClick={onClick} className="currency"> <label> <input onChange={handleCheckboxChange} checked={isSelected} type="checkbox" /> <span className="checkbox-container"></span> </label> <span>{currency.name}</span> </div> ); }; export default CurrencyItem;
货币数组常量
import { Currency } from "../types"; export const currencies: Currency[] = [ { name: "EUR", }, { name: "PLN", }, { name: "GEL", }, { name: "DKK", }, { name: "CZK", }, { name: "GBP", }, { name: "SEK", }, { name: "USD", }, { name: "RUB", }, ];
当前错误的测试代码
it("adds currency to select list", () => { const handleCurrencyToggle = jest.fn() const { getByTestId: getByTestId2 } = render( <CurrencySelector /> ); const { getByTestId } = render( <CurrencyItem key={1} currency={currencies[0]} onClick={() => () => { handleCurrencyToggle(); }} isSelected={true} /> ); const currencyItem = getByTestId("currency-item"); fireEvent.click(currencyItem); const selectItem = getByTestId2("selected-currency").textContent expect(selectItem).toBe("EUR"); });
问题分析与解决方案
当前测试的核心问题
- 分别渲染了
CurrencySelector和CurrencyItem两个独立实例,二者状态完全隔离,点击单独渲染的CurrencyItem不会影响CurrencySelector的状态。 onClick回调写成了嵌套函数() => () => { handleCurrencyToggle(); },点击时仅返回函数而非执行handleCurrencyToggle。- 直接Mock的
handleCurrencyToggle未关联到CurrencySelector内部的真实函数,因为该函数是组件私有状态,无法直接外部Mock。
正确测试思路:优先验证组件行为
推荐直接测试组件的实际表现而非内部函数,比如点击后选中区域是否出现对应货币、sessionStorage是否更新:
import { render, fireEvent, screen } from '@testing-library/react'; import CurrencySelector from './CurrencySelector'; import { currencies } from '../constants'; // 清除sessionStorage避免测试干扰 beforeEach(() => { sessionStorage.clear(); }); it('点击CurrencyItem后添加对应货币到选中列表', () => { render(<CurrencySelector />); // 获取第一个货币项(EUR) const eurItem = screen.getByTestId('currency-item-0'); // 触发点击 fireEvent.click(eurItem); // 验证选中区域出现EUR const selectedCurrency = screen.getByTestId('selected-currency'); expect(selectedCurrency.textContent).toContain('EUR'); // 可选:验证sessionStorage更新 const storedCurrencies = JSON.parse(sessionStorage.getItem('currencies') || '[]'); expect(storedCurrencies).toEqual([currencies[0]]); }); it('再次点击选中的CurrencyItem会移除该货币', () => { render(<CurrencySelector />); const eurItem = screen.getByTestId('currency-item-0'); fireEvent.click(eurItem); fireEvent.click(eurItem); // 验证回到无选中状态 expect(screen.getByTestId('no-selected-currencies')).toBeInTheDocument(); });
若需Mock内部函数:重构组件注入props
如果确实需要MockhandleCurrencyToggle,可以重构组件将该函数作为props传入,从而在测试中注入Mock函数:
重构后的CurrencySelector.tsx(核心修改)
// 新增props接受外部传入的toggle函数 const CurrencySelector = ({ handleCurrencyToggle: externalToggle }: { handleCurrencyToggle?: (currency: Currency) => void }) => { const initialSelected = () => { const storedCurrencies = sessionStorage.getItem("currencies"); return storedCurrencies ? JSON.parse(storedCurrencies) : []; }; const [selected, setSelected] = useState<Currency[]>(initialSelected); // 优先使用外部传入的函数,否则用内部实现 const handleCurrencyToggle = externalToggle || ((currency: Currency) => { const isSelected = selected.some( (selectedCurrency) => selectedCurrency.name === currency.name ); if (isSelected) { setSelected((prevSelected) => prevSelected.filter( (selectedCurrency) => selectedCurrency.name !== currency.name ) ); } else { setSelected((prevSelected) => [...prevSelected, currency]); } }); // 其余代码保持不变... };
对应的Mock测试代码
it('触发CurrencyItem点击时调用Mock的handleCurrencyToggle', () => { const mockToggle = jest.fn(); render(<CurrencySelector handleCurrencyToggle={mockToggle} />); const eurItem = screen.getByTestId('currency-item-0'); fireEvent.click(eurItem); // 验证Mock函数被调用,且参数正确 expect(mockToggle).toHaveBeenCalledWith(currencies[0]); });
内容的提问来源于stack exchange,提问作者Haider Mansoor
相关产品推荐
相关产品推荐

