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

使用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);
  });
});

失败原因

  1. 无效的props传递:TaskForm组件并未接收onClick属性,测试中传入的spy函数handleClick完全没有被组件使用,自然不会被调用。
  2. 测试目标错误:Add按钮的核心逻辑是触发Redux的dispatch(add(task.current)),应该验证这个dispatch行为,而非给组件传一个不存在的props。
  3. 组件小问题:按钮点击时设置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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.03 12:58:16