使用Vitest+React-Redux的按钮单元测试失败求助
解决React-Redux组件Add按钮单元测试失败问题
问题概述
作为单元测试新手,在使用Vitest、react-testing-library和jest-dom测试React-Redux应用的TaskForm组件Add按钮时,测试无法通过,报错:
AssertionError: expected "spy" to be called 1 times, but got 0 times
TaskForm组件代码
import { useDispatch } from "react-redux"; import { add } from "./taskFormSlice"; import { useRef, useState } from "react"; import Tasks from "./Tasks"; const TaskForm = () => { const dispatch = useDispatch(); const task = useRef<string>(""); // local state to reset input field after submitting form const [userInputVal, setUserInputVal] = useState(""); return ( <div className="flex flex-col space-y-8 items-center justify-center min-h-screen bg-indigo-900"> <div className="text-7xl text-white mt-10">To Do App</div> <div className="flex flex-row space-y-4 items-center justify-center"> <form onSubmit={(e) => e.preventDefault()}> <input value={userInputVal} onChange={(e) => { setUserInputVal(e.target.value); task.current = e.target.value; }} className=" w-96 border-solid border-sky-500 rounded-full border-2 px-8 py-2 placeholder:text-center text-md" placeholder="Enter Task" /> <div className="inline-block"> <button className="px-4 py-2 bg-blue-700 rounded-full text-white shadow-lg hover:shadow-sky-700 ml-40 mt-2 sm:ml-2" type="submit" onClick={() => { setUserInputVal(" "); dispatch(add(task.current)); }} > Add </button> </div> </form> </div> <Tasks></Tasks> </div> ); }; export default TaskForm;
原测试代码
import { describe, expect, test, vi } from "vitest"; import { fireEvent, screen, waitFor } from "@testing-library/react"; import TaskForm from "./TaskForm"; // Unit Test on React components // Unit test to test that input box takes in a input describe.sequential("to do list components render correctly", () => { test("input box takes in user input", async () => { renderWithProviders(<TaskForm></TaskForm>); const input = screen.getByPlaceholderText("Enter Task"); fireEvent.change(input, { target: { value: "test todo" } }); await waitFor(() => { expect(input).toHaveValue("test todo"); }); }); test("Test button click to ensure that it works", () => { const handleClick = vi.fn(); renderWithProviders(<TaskForm onClick={handleClick}></TaskForm>); fireEvent.click(screen.getByText("Add")); expect(handleClick).toHaveBeenCalledTimes(1); }); });
失败原因
- 无效的props传递:TaskForm组件并未接收
onClick属性,测试中传入的spy函数handleClick完全没有被组件使用,自然不会被调用。 - 测试目标错误:Add按钮的核心逻辑是触发Redux的
dispatch(add(task.current)),应该验证这个dispatch行为,而非给组件传一个不存在的props。 - 组件小问题:按钮点击时设置
setUserInputVal(" ")(空格)而非空字符串,可能导致输入框状态异常,也不符合预期逻辑。
解决方案
1. 修正组件的输入重置逻辑
将按钮onClick中的setUserInputVal(" ")改为setUserInputVal(""),确保输入框清空:
onClick={() => { setUserInputVal(""); // 改为空字符串 dispatch(add(task.current)); }}
2. 正确编写按钮点击测试
测试的核心是验证Redux的dispatch是否被正确调用,需要模拟或捕获dispatch的调用情况。以下两种方法任选其一:
方法一:模拟useDispatch钩子
import { describe, expect, test, vi } from "vitest"; import { fireEvent, screen, waitFor } from "@testing-library/react"; import TaskForm from "./TaskForm"; import { useDispatch } from "react-redux"; import { add } from "./taskFormSlice"; // 模拟useDispatch钩子 vi.mock("react-redux", () => ({ ...vi.importActual("react-redux"), useDispatch: vi.fn(), })); describe.sequential("to do list components render correctly", () => { test("input box takes in user input", async () => { renderWithProviders(<TaskForm></TaskForm>); const input = screen.getByPlaceholderText("Enter Task"); fireEvent.change(input, { target: { value: "test todo" } }); await waitFor(() => { expect(input).toHaveValue("test todo"); }); }); test("Add button triggers dispatch with correct task", () => { // 创建mock dispatch函数 const mockDispatch = vi.fn(); // 让useDispatch返回这个mock函数 (useDispatch as vi.Mock).mockReturnValue(mockDispatch); renderWithProviders(<TaskForm></TaskForm>); // 先输入测试任务 const input = screen.getByPlaceholderText("Enter Task"); fireEvent.change(input, { target: { value: "test task" } }); // 点击Add按钮 fireEvent.click(screen.getByText("Add")); // 验证dispatch被调用,且调用参数正确 expect(mockDispatch).toHaveBeenCalledTimes(1); expect(mockDispatch).toHaveBeenCalledWith(add("test task")); // 验证输入框被清空 expect(input).toHaveValue(""); }); });
方法二:通过renderWithProviders注入mock store
如果你的renderWithProviders支持传入自定义store,可以创建一个带mock dispatch的store:
import { configureStore } from "@reduxjs/toolkit"; import taskFormReducer from "./taskFormSlice"; // 创建带mock dispatch的测试store const createTestStore = () => { const store = configureStore({ reducer: { tasks: taskFormReducer }, }); // 替换dispatch为mock函数 store.dispatch = vi.fn(store.dispatch); return store; }; // 在测试中使用 test("Add button triggers dispatch with correct task", () => { const testStore = createTestStore(); renderWithProviders(<TaskForm></TaskForm>, { store: testStore }); const input = screen.getByPlaceholderText("Enter Task"); fireEvent.change(input, { target: { value: "test task" } }); fireEvent.click(screen.getByText("Add")); expect(testStore.dispatch).toHaveBeenCalledTimes(1); expect(testStore.dispatch).toHaveBeenCalledWith(add("test task")); });
关键要点
- 测试组件时,要针对组件的实际逻辑(这里是Redux dispatch)进行验证,而非凭空传递无效props。
- 使用Vitest的
vi.mock可以轻松模拟React-Redux的钩子,捕获dispatch调用。 - 测试不仅要验证行为是否触发,还要验证触发时的参数是否正确(比如传入的task内容)。
内容的提问来源于stack exchange,提问作者ToXiiXz
相关产品推荐
相关产品推荐

