React
Testing a react-hook-form form
A react-hook-form form is tested through the DOM: type with userEvent, click submit, await the error with findByText. Do not mock the library.
Key points
- Test the form the way a person uses it: type, click, read the message.
- Validation messages arrive a tick later, so they need findByText and await.
- Mocking react-hook-form switches off the very logic the test must cover.
A react-hook-form form is tested through the DOM: type with userEvent, click submit, await the message with findByText. Mocking the library removes the very part the test should cover.
The base rule
react-hook-form, under the MIT licence, registers inputs and validates on submit or blur. The error message appears after a microtask, so a synchronous getByText almost always misses it.
The test that works as a model
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
test("rifiuta l'email vuota", async () => {
const onSubmit = vi.fn();
render();
await userEvent.click(screen.getByRole("button", { name: "Crea account" }));
expect(await screen.findByText("Email obbligatoria")).toBeInTheDocument();
expect(onSubmit).not.toHaveBeenCalled();
});
The test touches neither register nor handleSubmit: it clicks the real button and checks two things, the callback was not called and the message is visible.
The four cases that are enough
- Empty field: submit skips the callback and shows the required message.
- Wrong format: an email without an at sign produces the format error, not the empty-field one.
- Valid submit: the callback receives the typed values, including the fields left untouched.
- Server error: a 422 response shows its message and keeps the data in the form.
Setup
npm i -D vitest jsdom @testing-library/react @testing-library/user-event react-hook-form
npx vitest run --environment jsdom
All MIT, all local: react-hook-form makes no network call and the test needs no browser.
Live records in the catalogue
Three forms to try the four cases on: login-form, credit-card-form and otp-input. To browse by category start from 111,985 components.
The limitation
userEvent types character by character, so a long field slows the suite down: use paste when the text is not the point of the test. jsdom also has no autofill and no password manager, so those bugs stay out of reach.
Where to continue
The shadcn form is in a react-hook-form form; controlled against uncontrolled is in controlled inputs; the test model is in Testing Library: the five tests; the measured library comparison is in React libraries 2026.
Frequently asked questions
Should react-hook-form be mocked?
No. The mock switches off validation and state, meaning the logic under test, so test the rendered form.
How is the form submitted in a test?
With a click on the submit button found by role. Calling submit() on the element skips the click handlers.
Why does the error message never appear?
Because validation is async: await findByText instead of calling getByText right after the click.
How is a masked field tested?
Type the final value and check the displayed text; the key sequence is an implementation detail.
Does the test cover server validation too?
No, it covers the stubbed response only. The real rule stays on the server, and that one is the rule that counts.