Test Skill
Run tests and ensure code quality meets the 80% coverage requirement.
Trigger
Use this skill when: user says "test", "run tests", "check coverage", "write tests", or "verify quality"
Quick Commands
bash
# Run all tests pnpm test # Run tests with coverage report pnpm test:coverage # Run tests in watch mode pnpm test -- --watch # Run specific test file pnpm test path/to/test.test.tsx # Run tests matching pattern pnpm test -- -t "pattern" # Open Vitest UI pnpm test:ui
Coverage Requirements
Minimum 80% coverage required for:
- •Statements
- •Branches
- •Functions
- •Lines
The CI/CD pipeline will fail if coverage drops below 80%.
Writing Tests
Test File Location
Tests go in __tests__/ directory, mirroring source structure:
code
__tests__/
├── components/
│ ├── button.test.tsx
│ └── header.test.tsx
├── hooks/
│ └── use-auth.test.tsx
├── lib/
│ └── utils.test.ts
└── app/
└── page.test.tsx
Component Test Template
tsx
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, it, expect, vi, beforeEach } from "vitest";
import { ComponentName } from "@/components/path/component-name";
// Mock dependencies if needed
vi.mock("next/navigation", () => ({
useRouter: () => ({ push: vi.fn() }),
}));
describe("ComponentName", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("renders correctly", () => {
render(<ComponentName />);
expect(screen.getByRole("button")).toBeInTheDocument();
});
it("handles user interaction", async () => {
const user = userEvent.setup();
const onClick = vi.fn();
render(<ComponentName onClick={onClick} />);
await user.click(screen.getByRole("button"));
expect(onClick).toHaveBeenCalledTimes(1);
});
it("displays loading state", () => {
render(<ComponentName isLoading />);
expect(screen.getByText(/loading/i)).toBeInTheDocument();
});
it("shows error message on failure", () => {
render(<ComponentName error="Something went wrong" />);
expect(screen.getByText(/something went wrong/i)).toBeInTheDocument();
});
});
Hook Test Template
tsx
import { renderHook, act } from "@testing-library/react";
import { describe, it, expect, vi } from "vitest";
import { useCustomHook } from "@/hooks/use-custom-hook";
describe("useCustomHook", () => {
it("returns initial state", () => {
const { result } = renderHook(() => useCustomHook());
expect(result.current.value).toBe(initialValue);
});
it("updates state correctly", () => {
const { result } = renderHook(() => useCustomHook());
act(() => {
result.current.setValue("new value");
});
expect(result.current.value).toBe("new value");
});
});
Async Test Template
tsx
import { render, screen, waitFor } from "@testing-library/react";
import { describe, it, expect, vi } from "vitest";
// Mock fetch or API calls
vi.mock("@/lib/api", () => ({
fetchData: vi.fn(),
}));
import { fetchData } from "@/lib/api";
import { DataComponent } from "@/components/data-component";
describe("DataComponent", () => {
it("fetches and displays data", async () => {
vi.mocked(fetchData).mockResolvedValue({ items: ["Item 1", "Item 2"] });
render(<DataComponent />);
// Wait for data to load
await waitFor(() => {
expect(screen.getByText("Item 1")).toBeInTheDocument();
});
});
it("handles errors gracefully", async () => {
vi.mocked(fetchData).mockRejectedValue(new Error("Failed"));
render(<DataComponent />);
await waitFor(() => {
expect(screen.getByText(/error/i)).toBeInTheDocument();
});
});
});
What to Test
Always Test
- •Component rendering
- •User interactions (clicks, typing, etc.)
- •Loading states
- •Error states
- •Edge cases (empty data, null values)
- •Accessibility (roles, labels)
Don't Test
- •Third-party library internals
- •CSS styling (unless critical)
- •Implementation details
- •Trivial code (getters, simple wrappers)
Mocking
Mock Next.js Navigation
tsx
vi.mock("next/navigation", () => ({
useRouter: () => ({
push: vi.fn(),
replace: vi.fn(),
back: vi.fn(),
}),
usePathname: () => "/",
useSearchParams: () => new URLSearchParams(),
}));
Mock Supabase
tsx
vi.mock("@/supabase/client", () => ({
createClient: () => ({
from: vi.fn(() => ({
select: vi.fn().mockResolvedValue({ data: [], error: null }),
insert: vi.fn().mockResolvedValue({ data: null, error: null }),
})),
auth: {
getUser: vi.fn().mockResolvedValue({ data: { user: null }, error: null }),
},
}),
}));
Mock Next-Themes
tsx
vi.mock("next-themes", () => ({
useTheme: () => ({ theme: "light", setTheme: vi.fn() }),
ThemeProvider: ({ children }: { children: React.ReactNode }) => children,
}));
Debugging Tests
bash
# Run with verbose output pnpm test -- --reporter=verbose # Run single test in isolation pnpm test -- path/to/test.test.tsx --run # Debug in browser pnpm test:ui
Checklist Before PR
- • All tests pass:
pnpm test - • Coverage >= 80%:
pnpm test:coverage - • No console errors/warnings
- • Lint passes:
pnpm lint - • Build succeeds:
pnpm build