You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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");
});

问题分析与解决方案

当前测试的核心问题

  1. 分别渲染了CurrencySelector和CurrencyItem两个独立实例,二者状态完全隔离,点击单独渲染的CurrencyItem不会影响CurrencySelector的状态。
  2. onClick回调写成了嵌套函数() => () => { handleCurrencyToggle(); },点击时仅返回函数而非执行handleCurrencyToggle。
  3. 直接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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.01 05:55:56