如何用React Testing Library+Jest测试React下拉组件?报错求助
解决React Testing Library测试下拉组件的问题
错误原因分析
你遇到的报错是因为**toHaveBeenCalledTimes只能用于断言mock函数的调用次数**,但你现在把DOM按钮元素(dropdwonBtn)传入了这个断言,所以Jest报错提示你传入的不是函数。
另外你的测试代码还存在两个问题:
- 没有用
Router包裹Dropdown组件,组件内的Link会因为缺少路由上下文而报错 - 测试逻辑偏离了RTL的核心原则:应该测试用户能看到的UI变化,而非内部函数调用
修正后的测试方案
1. 修复路由上下文问题
测试时需要用MemoryRouter(更适合测试场景)包裹组件,提供路由上下文:
2. 测试下拉菜单的显示/隐藏行为
RTL推荐测试用户可见的结果,所以点击按钮后,应该断言下拉项容器是否出现:
import { render, screen, fireEvent } from "@testing-library/react"; import { MemoryRouter } from "react-router-dom"; import Dropdown from "./Dropdown"; const testOptions = [ { value: "item1", label: "item 1", to: "/about" }, { value: "item2", label: "item 2", to: "/contact" }, // 修正重复value,避免React key警告 ]; it("displays dropdown items when button is clicked", () => { render( <MemoryRouter> <Dropdown placeholder="placeholder name" options={testOptions} /> </MemoryRouter> ); const dropdownBtn = screen.getByTestId("dropdownBtn"); // 初始状态下拉容器不存在 expect(screen.queryByTestId("dropdownItems")).not.toBeInTheDocument(); fireEvent.click(dropdownBtn); // 断言下拉容器及选项可见 expect(screen.getByTestId("dropdownItems")).toBeInTheDocument(); expect(screen.getByText("item 1")).toBeInTheDocument(); }); it("hides dropdown items when button is clicked again", () => { render( <MemoryRouter> <Dropdown placeholder="placeholder name" options={testOptions} /> </MemoryRouter> ); const dropdownBtn = screen.getByTestId("dropdownBtn"); fireEvent.click(dropdownBtn); expect(screen.getByTestId("dropdownItems")).toBeInTheDocument(); // 再次点击关闭下拉 fireEvent.click(dropdownBtn); expect(screen.queryByTestId("dropdownItems")).not.toBeInTheDocument(); });
3. 若需测试点击事件函数的调用次数(可选)
如果你确实需要验证内部点击函数的调用次数,可将函数作为props传入组件,再用mock函数测试:
修改Dropdown组件,支持外部传入点击回调:
import { useState } from "react"; import { Link } from "react-router-dom"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faChevronDown } from "@fortawesome/free-solid-svg-icons"; export default function Dropdown({ placeholder, options, onDropdownClick }) { const [showDropdown, setShowDropdown] = useState(false); const handleDropdownClick = () => { setShowDropdown(prev => !prev); onDropdownClick?.(); }; return ( <div> <button data-testid="dropdownBtn" className="dropdownBtn" onClick={handleDropdownClick}> {placeholder} <span> <FontAwesomeIcon icon={faChevronDown} /> </span> </button> {showDropdown && ( <div data-testid="dropdownItems" className="dropdownItems"> {options.map((option) => ( <Link to={option.to} key={option.value}> {option.label} </Link> ))} </div> )} </div> ); }
测试mock函数的调用次数:
it("triggers onDropdownClick when button is clicked", () => { const mockClickHandler = jest.fn(); render( <MemoryRouter> <Dropdown placeholder="placeholder name" options={testOptions} onDropdownClick={mockClickHandler} /> </MemoryRouter> ); const dropdownBtn = screen.getByTestId("dropdownBtn"); fireEvent.click(dropdownBtn); expect(mockClickHandler).toHaveBeenCalledTimes(1); });
额外注意事项
- 优先使用
screen.getByText而非data-testid查询元素,更贴近用户实际操作逻辑 - 使用
queryBy*方法查询可能不存在的元素,getBy*会在元素不存在时直接抛出错误 - 确保组件内的列表项key唯一,避免React警告
内容的提问来源于stack exchange,提问作者Tapesh Patel
相关产品推荐
相关产品推荐

