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

vitest中react-hook-form的zodResolver验证异常问题排查

Zod + React Hook Form + Vitest 集成测试失败排查方案

问题背景

我有一个Zod验证Schema,在运行环境和单元测试中都正常,但和React Hook Form结合做Vitest集成测试时,以下两个用例始终失败:

  • 输入正确时调用onSubmit
  • 输入无效时不调用onSubmit

相关代码片段

1. Zod Schema 定义

import { z } from "zod";

const UserSchema = z.object({
  name: z.string().min(2, "名称至少需要2个字符"),
  email: z.string().email("请输入有效的邮箱地址"),
  age: z.number().min(18, "必须年满18岁")
});

export type UserFormData = z.infer<typeof UserSchema>;
export default UserSchema;

2. 测试数据

const validData = {
  name: "张三",
  email: "zhangsan@example.com",
  age: 25
};

const invalidData = {
  name: "李",
  email: "invalid-email",
  age: 17
};

3. Zod Schema 单元测试(正常通过)

import { describe, it, expect } from "vitest";
import UserSchema from "./userSchema";

describe("UserSchema 验证", () => {
  it("验证有效数据通过", () => {
    const result = UserSchema.safeParse(validData);
    expect(result.success).toBe(true);
  });

  it("验证无效数据失败", () => {
    const result = UserSchema.safeParse(invalidData);
    expect(result.success).toBe(false);
    expect(result.error?.issues).toHaveLength(3);
  });
});

4. 表单组件实现

import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import UserSchema, { UserFormData } from "./userSchema";

export default function UserForm({ onSubmit }: { onSubmit: (data: UserFormData) => void }) {
  const { register, handleSubmit, formState: { errors } } = useForm<UserFormData>({
    resolver: zodResolver(UserSchema)
  });

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div>
        <label>名称</label>
        <input {...register("name")} />
        {errors.name && <span>{errors.name.message}</span>}
      </div>
      <div>
        <label>邮箱</label>
        <input {...register("email")} />
        {errors.email && <span>{errors.email.message}</span>}
      </div>
      <div>
        <label>年龄</label>
        <input type="number" {...register("age", { valueAsNumber: true })} />
        {errors.age && <span>{errors.age.message}</span>}
      </div>
      <button type="submit">提交</button>
    </form>
  );
}

5. 集成测试代码(失败用例)

import { describe, it, expect, vi } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
import UserForm from "./UserForm";

describe("UserForm 集成测试", () => {
  it("输入正确数据时调用onSubmit", async () => {
    const mockSubmit = vi.fn();
    render(<UserForm onSubmit={mockSubmit} />);

    fireEvent.change(screen.getByLabelText("名称"), { target: { value: validData.name } });
    fireEvent.change(screen.getByLabelText("邮箱"), { target: { value: validData.email } });
    fireEvent.change(screen.getByLabelText("年龄"), { target: { value: validData.age.toString() } });

    fireEvent.submit(screen.getByText("提交"));

    expect(mockSubmit).toHaveBeenCalledTimes(1); // 失败:调用次数为0
    expect(mockSubmit).toHaveBeenCalledWith(validData);
  });

  it("输入无效数据时不调用onSubmit", async () => {
    const mockSubmit = vi.fn();
    render(<UserForm onSubmit={mockSubmit} />);

    fireEvent.change(screen.getByLabelText("名称"), { target: { value: invalidData.name } });
    fireEvent.change(screen.getByLabelText("邮箱"), { target: { value: invalidData.email } });
    fireEvent.change(screen.getByLabelText("年龄"), { target: { value: invalidData.age.toString() } });

    fireEvent.submit(screen.getByText("提交"));

    expect(mockSubmit).not.toHaveBeenCalled(); // 失败:调用了1次
  });
});

排查与修复方案

1. 处理异步验证的时序问题

React Hook Form结合Zod resolver的验证是异步的,测试断言会在验证完成前执行,导致结果不符合预期。用waitFor包裹断言,等待验证流程完成:

import { waitFor } from "@testing-library/react";

// 有效数据测试用例修改
await waitFor(() => {
  expect(mockSubmit).toHaveBeenCalledTimes(1);
});

// 无效数据测试用例修改
await waitFor(() => {
  expect(mockSubmit).not.toHaveBeenCalled();
});

2. 确保表单提交事件正确触发

有时候按钮文本匹配可能出问题,直接获取表单元素触发提交更可靠:

fireEvent.submit(screen.getByRole("form"));

3. 模拟用户完整输入流程

添加blur事件模拟用户离开输入框的行为,确保React Hook Form及时更新字段状态:

// 年龄输入框示例
fireEvent.change(screen.getByLabelText("年龄"), { target: { value: validData.age.toString() } });
fireEvent.blur(screen.getByLabelText("年龄"));

4. 检查版本兼容性

确保react-hook-form、@hookform/resolvers、zod三者版本兼容,建议升级到最新稳定版:

npm update react-hook-form @hookform/resolvers zod

5. 验证字段类型转换

对于valueAsNumber的字段,可在提交前断言字段值是否正确转换,确保状态同步:

await waitFor(() => {
  expect(screen.getByLabelText("年龄")).toHaveValue(validData.age.toString());
});

内容的提问来源于stack exchange,提问作者gomgun...

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.20 05:43:15