This project uses Vitest as the testing framework combined with React Testing Library for component testing. Our testing philosophy follows the principle of testing behavior rather than implementation details.
Tests React components for rendering, user interactions, and prop handling.
Files:
components/date-picker.test.tsxapp/features/ticket/components/ticket-upsert-form.test.tsxapp/features/ticket/components/ticket-item.test.tsx
Focus:
- Rendering and conditional rendering
- User interactions (clicks, input changes)
- Props handling and validation
- Accessibility attributes
Tests Next.js Server Actions for data validation, transformation, and database operations.
Files:
app/features/ticket/queries/actions/upsert-ticket.test.ts
Focus:
- Input validation (Zod schemas)
- Data transformation (currency conversion)
- Database interactions (upsert, delete)
- Error handling
- Side effects (redirects, cookies, revalidation)
When to use: Testing UI components that depend on external libraries or complex sub-components.
// Mock external dependencies
vi.mock("date-fns", () => ({
format: vi.fn((date: Date, pattern: string) => {
// Custom implementation
}),
}));
// Mock UI components
vi.mock("@/components/ui/button", () => ({
Button: ({ children, ...props }: any) => (
<button {...props}>{children}</button>
),
}));Test Structure:
describe("DatePicker", () => {
describe("Rendering", () => {
it("should render with required props", () => {
render(<DatePicker id="test" name="date" />);
expect(screen.getByRole("button")).toBeInTheDocument();
});
});
describe("Initial State", () => {
it("should initialize with today's date", () => {
// Test default behavior
});
});
describe("User Interactions", () => {
it("should update date on selection", () => {
// Test interactive behavior
});
});
});When to use: Testing business logic, validation, and database operations.
// Mock external dependencies
vi.mock("../lib/prisma", () => ({
prisma: {
ticket: { upsert: vi.fn() },
},
}));
// Setup
beforeEach(() => {
vi.clearAllMocks();
mockActionState = { status: "INITIAL", payload: new FormData() };
});
// Test execution
it("should validate input", async () => {
const formData = new FormData();
formData.append("title", "");
const result = await upsertTicket(undefined, mockActionState, formData);
expect(result.status).toBe("ERROR");
});Verify components display correctly and contain expected elements.
it("should render with required props", () => {
render(<Component prop="value" />);
expect(screen.getByRole("button")).toBeInTheDocument();
});Verify component responds correctly to props and manages internal state.
it("should initialize with provided defaultValue", () => {
render(<DatePicker defaultValue="2026-04-15" />);
expect(screen.getByText("2026-04-15")).toBeInTheDocument();
});Verify user interactions trigger expected behaviors.
it("should update date on selection", () => {
render(<DatePicker />);
fireEvent.click(screen.getByTestId("select-button"));
expect(screen.getByDisplayValue(/\d{4}-\d{2}-\d{2}/)).toBeInTheDocument();
});Verify input validation and error handling.
it("should reject empty title", async () => {
const formData = new FormData();
formData.append("title", "");
const result = await upsertTicket(undefined, state, formData);
expect(result.fieldErrors?.title).toBeDefined();
});Verify multiple components/systems work together.
it("should include hidden input in form submission", () => {
render(
<form>
<DatePicker name="deadline" />
</form>
);
const formData = new FormData(form);
expect(formData.get("deadline")).toBeDefined();
});Verify components are accessible to assistive technologies.
it("should have accessible button", () => {
render(<DatePicker id="date-picker" />);
const button = screen.getByRole("button");
expect(button).toHaveAttribute("id", "date-picker");
});-
External Libraries (date-fns, icons)
vi.mock("date-fns", () => ({ format: vi.fn(), }));
-
UI Component Libraries
vi.mock("@/components/ui/button", () => ({ Button: ({ children, ...props }) => <button {...props}>{children}</button>, }));
-
Database/Server Operations
vi.mock("../lib/prisma", () => ({ prisma: { ticket: { upsert: vi.fn() } }, }));
-
Next.js Features (navigation, cache)
vi.mock("next/navigation", () => ({ redirect: vi.fn(), }));
- Core React behavior (hooks, state)
- User interactions (clicks, form submissions)
- Business logic validation
- Component composition
-
Test behavior, not implementation
// ✅ Good: Tests what user sees expect(screen.getByRole("button", { name: /submit/i })).toBeInTheDocument(); // ❌ Bad: Tests implementation expect(component.state.isOpen).toBe(true);
-
Use semantic queries
// ✅ Good screen.getByRole("button"); screen.getByLabelText("Title"); // ❌ Bad screen.getByTestId("btn-submit");
-
Group related tests
describe("DatePicker", () => { describe("Rendering", () => { /* ... */ }); describe("User Interactions", () => { /* ... */ }); });
-
Clear test names that describe the scenario
// ✅ Good it("should close popover after date selection", () => {}); // ❌ Bad it("works correctly", () => {});
-
Use beforeEach for setup
beforeEach(() => { vi.clearAllMocks(); mockActionState = getDefaultState(); });
-
Don't test implementation details
- Avoid testing internal state directly
- Avoid testing private methods
-
Don't over-mock
- Only mock external dependencies
- Keep React core behavior unm
ocked
- Don't create brittle tests
- Avoid hard-coded indices
- Avoid testing CSS classes directly
- Renders with required props
- Handles optional props correctly
- Manages internal state properly
- Responds to user interactions
- Has accessible markup
- Works in different states (loading, error, success)
- Validates input if applicable
- Validates all required fields
- Rejects invalid input formats
- Transforms data correctly
- Handles success case
- Handles error cases
- Triggers side effects (redirects, cookies, cache revalidation)
- Pass correct data to database
# Run all tests
npm run test
# Run tests in watch mode
npm run test:watch
# Run tests for specific file
npm run test date-picker.test.tsx
# Run tests with coverage
npm run test:coverage- Components: 80%+ coverage
- Server Actions/Services: 90%+ coverage
- Utilities: 95%+ coverage