Agent skill
writing-tests
Write user-centric tests following Kent C. Dodds principles. Use when asked to write tests for components, hooks, or features. Generates tests that test behavior, use accessibility queries, and mock only at boundaries.
Stars
163
Forks
31
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/data/writing-tests
SKILL.md
Writing Tests Skill
Write tests that give confidence the app works for users, not that code is structured a certain way.
Core Philosophy
"The more your tests resemble the way your software is used, the more confidence they can give you." - Kent C. Dodds
- Test behavior, not implementation - Test what users see and do
- Use accessibility queries -
getByRole,getByLabelTextovergetByTestId - Mock at boundaries only - Network/Convex hooks, not components
- Integration tests > unit tests - Test real workflows
Query Priority
| Priority | Query | When |
|---|---|---|
| 1 | getByRole |
Semantic HTML elements |
| 2 | getByLabelText |
Form fields |
| 3 | getByText |
Buttons, links |
| 4 | getByTestId |
LAST RESORT |
Existing Infrastructure (USE THESE)
DO NOT create new test utilities. Use:
typescript
// Factories - src/lib/test/factories.ts
import { createOptimisticMessage, createTestMessageData, createTestUserData, createTestConversationData, createMockIdentity } from "@/lib/test/factories";
// API helpers - src/lib/test/api-helpers.ts
import { createMockRequest, assertEnvelopeSuccess, assertEnvelopeError, unwrapData } from "@/lib/test/api-helpers";
Test Patterns
Component Tests
typescript
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi, beforeEach } from "vitest";
// Mock BEFORE importing component
vi.mock("convex/react", () => ({
useQuery: vi.fn(() => null),
useMutation: vi.fn(() => vi.fn()),
}));
const mockMutation = vi.fn();
vi.mock("@/lib/hooks/mutations", () => ({
useSendMessage: () => ({ mutate: mockMutation, isPending: false }),
}));
// Import AFTER mocks
import { MyComponent } from "../MyComponent";
describe("MyComponent", () => {
beforeEach(() => vi.clearAllMocks());
it("sends data when user submits", async () => {
const user = userEvent.setup();
render(<MyComponent {...props} />);
// Use accessibility queries
const input = screen.getByLabelText("Message input");
await user.type(input, "Hello{Enter}");
expect(mockMutation).toHaveBeenCalledWith(
expect.objectContaining({ content: "Hello" })
);
});
});
Convex Tests
typescript
import { convexTest } from "convex-test";
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
import { api } from "../_generated/api";
import schema from "../schema";
import { createMockIdentity, createTestUserData, createTestConversationData } from "@/lib/test/factories";
describe("conversations", () => {
// Use fake timers if testing scheduled functions
beforeEach(() => vi.useFakeTimers());
afterEach(() => vi.useRealTimers());
it("returns only user's conversations", async () => {
const t = convexTest(schema);
const identity = createMockIdentity();
await t.run(async (ctx) => {
const userId = await ctx.db.insert("users", createTestUserData({
clerkId: identity.subject,
}));
await ctx.db.insert("conversations", createTestConversationData(userId));
});
const asUser = t.withIdentity(identity);
// @ts-ignore - Type depth exceeded with 94+ Convex modules
const result = await asUser.query(api.conversations.list, {});
expect(result).toHaveLength(1);
});
});
API Route Tests
typescript
// Mocks MUST be BEFORE imports
vi.mock("@/lib/api/dal/conversations", () => ({
conversationsDAL: { list: vi.fn(), create: vi.fn() },
}));
import { conversationsDAL } from "@/lib/api/dal/conversations";
import { createMockRequest, assertEnvelopeSuccess } from "@/lib/test/api-helpers";
describe("/api/v1/conversations", () => {
beforeEach(() => vi.clearAllMocks());
it("returns list with envelope", async () => {
vi.mocked(conversationsDAL.list).mockResolvedValue([]);
// Dynamic import AFTER mock setup
const { GET } = await import("../route");
const response = await GET(createMockRequest("/api/v1/conversations"));
const json = await response.json();
assertEnvelopeSuccess(json);
});
});
What to Mock
Mock:
convex/reacthooks (useQuery, useMutation)- Custom mutation hooks (
@/lib/hooks/mutations) - DAL modules (
@/lib/api/dal/*) - Browser APIs (localStorage, clipboard)
- System clock (
vi.useFakeTimers())
Don't Mock:
- Child components
- Domain objects
- React itself
- CSS/styling
Anti-Patterns to Avoid
typescript
// BAD: Over-mocking
vi.mock("./ChildA");
vi.mock("./ChildB");
vi.mock("../hooks/useX");
// BAD: Testing implementation
expect(component.state.loading).toBe(true);
// BAD: Snapshot for behavior
expect(component).toMatchSnapshot();
// BAD: Test IDs when a11y query works
screen.getByTestId("submit-button");
File Locations
| Test Type | Location |
|---|---|
| Component | src/components/[name]/__tests__/[Name].test.tsx |
| Convex | convex/__tests__/[name].test.ts |
| API Route | src/app/api/v1/__tests__/[name].test.ts |
| Utility | src/lib/[path]/__tests__/[name].test.ts |
| E2E | e2e/[name].spec.ts |
Reference
Full philosophy: docs/testing/testing-philosophy.md
Phase docs: docs/testing/phase-*.md
Didn't find tool you were looking for?