| name | vitest |
| description | Vitest testing patterns with React Testing Library. Trigger: When writing unit tests - AAA pattern, mocking, async testing.
|
| license | Apache-2.0 |
| metadata | {"author":"prowler-cloud","version":"1.1","scope":["root"],"auto_invoke":"Writing unit tests with Vitest"} |
Test Structure (REQUIRED)
Use the Given/When/Then (AAA - Arrange/Act/Assert) pattern with comments:
it("should update user name when form is submitted", async () => {
const user = userEvent.setup();
const onSubmit = vi.fn();
render(<UserForm onSubmit={onSubmit} />);
await user.type(screen.getByLabelText(/name/i), "John Doe");
await user.click(screen.getByRole("button", { name: /submit/i }));
expect(onSubmit).toHaveBeenCalledWith({ name: "John Doe" });
});
Describe Block Organization (REQUIRED)
describe("ComponentName", () => {
describe("when user is authenticated", () => {
it("should display user profile", () => {});
it("should show logout button", () => {});
});
describe("when user is not authenticated", () => {
it("should redirect to login", () => {});
});
describe("form validation", () => {
it("should show error for invalid email", () => {});
it("should disable submit when fields are empty", () => {});
});
});
Naming Conventions (REQUIRED)
it("should display error message when API fails", () => {});
it("should disable button while loading", () => {});
it("should call onSubmit with form data", () => {});
it("should set isLoading to true", () => {});
it("should call useState", () => {});
it("should render div with class error", () => {});
React Testing Library Queries (REQUIRED)
screen.getByRole("button", { name: /submit/i });
screen.getByLabelText(/email/i);
screen.getByPlaceholderText(/search/i);
screen.getByText(/welcome/i);
screen.getByTestId("custom-element");
container.querySelector(".btn-primary");
document.getElementById("submit");
userEvent over fireEvent (REQUIRED)
const user = userEvent.setup();
await user.click(button);
await user.type(input, "hello");
await user.selectOptions(select, "option1");
await user.keyboard("{Enter}");
fireEvent.click(button);
fireEvent.change(input, { target: { value: "hello" } });
Async Testing Patterns (REQUIRED)
const element = await screen.findByText(/loaded/i);
await waitFor(() => {
expect(screen.getByText(/success/i)).toBeInTheDocument();
});
await waitFor(() => expect(mockFn).toHaveBeenCalled());
await waitFor(() => expect(screen.getByText(/done/i)).toBeVisible());
await waitFor(() => {
expect(mockFn).toHaveBeenCalled();
expect(screen.getByText(/done/i)).toBeVisible();
});
await waitFor(() => {});
Mocking with vi.fn() (REQUIRED)
const handleClick = vi.fn();
render(<Button onClick={handleClick} />);
await user.click(screen.getByRole("button"));
expect(handleClick).toHaveBeenCalledTimes(1);
const fetchUser = vi.fn().mockResolvedValue({ name: "John" });
const calculate = vi.fn().mockImplementation((a, b) => a + b);
afterEach(() => {
vi.restoreAllMocks();
});
vi.spyOn vs vi.mock
const spy = vi.spyOn(service, "fetchData").mockResolvedValue(mockData);
vi.mock("@/services/api", () => ({
fetchUser: vi.fn().mockResolvedValue({ name: "John" }),
}));
vi.mock("./module", () => ({
fn: vi.fn().mockReturnValue(someVariable),
}));
vi.mock("./module", async () => {
return {
fn: vi.fn(),
};
});
Testing Hooks Pattern
import { renderHook, act } from "@testing-library/react";
describe("useCounter", () => {
it("should increment counter", () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
});
Snapshot Testing (USE SPARINGLY)
it("should format date correctly", () => {
expect(formatDate(new Date("2024-01-15"))).toMatchInlineSnapshot(
`"January 15, 2024"`
);
});
it("should render correct HTML", () => {
const { container } = render(<Card title="Test" />);
expect(container).toMatchFileSnapshot("./snapshots/card.html");
});
expect(container).toMatchSnapshot();
Test Isolation (REQUIRED)
beforeEach(() => {
vi.clearAllMocks();
});
afterEach(() => {
vi.restoreAllMocks();
cleanup();
});
describe("Counter", () => {
let counter = 0;
it("should start at zero", () => {
render(<Counter />);
expect(screen.getByText("0")).toBeInTheDocument();
});
});
What NOT to Test
expect(component.state.isLoading).toBe(true);
expect(useState).toHaveBeenCalled();
expect(axios.get).toHaveBeenCalled();
expect(screen.getByText("Welcome")).toBeInTheDocument();
expect(screen.getByRole("button")).toBeDisabled();
expect(screen.getByText(/error/i)).toBeVisible();
Coverage Configuration
export default defineConfig({
test: {
coverage: {
provider: "v8",
reporter: ["text", "json", "html"],
all: true,
include: ["src/**/*.{ts,tsx}"],
exclude: [
"**/*.test.{ts,tsx}",
"**/*.d.ts",
"**/types/**",
"**/index.ts",
],
thresholds: {
lines: 80,
functions: 80,
branches: 80,
statements: 80,
},
},
},
});
File Organization
components/
├── Button/
│ ├── Button.tsx
│ ├── Button.test.tsx # Co-located test
│ └── index.ts
├── Form/
│ ├── Form.tsx
│ ├── Form.test.tsx
│ └── index.ts
Common Matchers
expect(element).toBeInTheDocument();
expect(element).toBeVisible();
expect(element).toBeEmptyDOMElement();
expect(button).toBeDisabled();
expect(input).toHaveValue("text");
expect(checkbox).toBeChecked();
expect(input).toHaveFocus();
expect(element).toHaveTextContent(/hello/i);
expect(element).toHaveAttribute("href", "/home");
expect(element).toHaveClass("active");
expect(fn).toHaveBeenCalled();
expect(fn).toHaveBeenCalledWith(arg1, arg2);
expect(fn).toHaveBeenCalledTimes(2);
Anti-Patterns to Avoid
expect(wrapper.instance().state.value).toBe(1);
const button = container.querySelector("button.primary");
await new Promise(r => setTimeout(r, 1000));
act(() => setState(1));
act(() => setState(2));
expect(mock.mock.calls[0][0]).toBe("first");
expect(entirePage).toMatchSnapshot();
Keywords
vitest, testing, unit test, react testing library, mock, spy, AAA, given when then, arrange act assert, userEvent, waitFor, findBy, coverage