Testing
Unit (Jest + RTL), integration (mocked repositories), and E2E (Playwright). Coverage thresholds, fixture strategy, and CI gating.
1. Testing Strategy
Three layers, each with a specific tool:
- Unit — Jest + React Testing Library. Fast, isolated. Pure functions, hooks, presentational components.
- Integration — Jest + RTL with mocked services. Component trees, form flows, state transitions.
- E2E — Playwright. Real browser, real network. Auth flows, payment, business-critical paths only.
2. Install
# Unit + integration
yarn add -D jest jest-environment-jsdom @types/jest \
@testing-library/react @testing-library/jest-dom @testing-library/user-event \
ts-jest
# E2E
yarn add -D @playwright/test
npx playwright install --with-deps3. Jest Config
Next.js ships next/jest which handles SWC compilation, CSS modules, and module aliases automatically.
1 // jest.config.ts 2 import type { Config } from "jest"; 3 import nextJest from "next/jest.js"; 4 5 const createJestConfig = nextJest({ dir: "./" }); 6 7 const config: Config = { 8 setupFilesAfterEach: ["<rootDir>/jest.setup.ts"], 9 testEnvironment: "jest-environment-jsdom", 10 moduleNameMapper: { 11 "^@/(.*)$": "<rootDir>/src/$1", 12 }, 13 coverageThreshold: { 14 global: { branches: 70, functions: 70, lines: 70, statements: 70 }, 15 }, 16 }; 17 18 export default createJestConfig(config); 19 20 // jest.setup.ts 21 import "@testing-library/jest-dom";
4. Component Test Example
1 // src/shared/ui/__tests__/button.test.tsx 2 import { render, screen } from "@testing-library/react"; 3 import userEvent from "@testing-library/user-event"; 4 import { Button } from "@/shared/ui/button"; 5 6 describe("Button", () => { 7 it("renders children", () => { 8 render(<Button>Click me</Button>); 9 expect(screen.getByRole("button", { name: /click me/i })).toBeInTheDocument(); 10 }); 11 12 it("calls onClick when clicked", async () => { 13 const user = userEvent.setup(); 14 const onClick = jest.fn(); 15 render(<Button onClick={onClick}>Submit</Button>); 16 await user.click(screen.getByRole("button")); 17 expect(onClick).toHaveBeenCalledTimes(1); 18 }); 19 20 it("is disabled when prop set", () => { 21 render(<Button disabled>Submit</Button>); 22 expect(screen.getByRole("button")).toBeDisabled(); 23 }); 24 });
5. Mocking Server Modules
Mock at the service boundary, never deeper. The repository pattern makes this clean: inject a fake repository in tests instead of mocking Prisma.
1 // src/features/users/services/__tests__/user.service.test.ts 2 import { UserService } from "../user.service"; 3 import type { UserRepository } from "../../server/user.repository"; 4 5 describe("UserService.findOrCreate", () => { 6 it("returns existing user when email is found", async () => { 7 const fakeRepo: UserRepository = { 8 findById: jest.fn(), 9 findByEmail: jest.fn().mockResolvedValue({ id: "u1", email: "a@b.com" }), 10 create: jest.fn(), 11 update: jest.fn(), 12 }; 13 const service = new UserService(fakeRepo); 14 const result = await service.findOrCreate("a@b.com"); 15 16 expect(result.id).toBe("u1"); 17 expect(fakeRepo.create).not.toHaveBeenCalled(); 18 }); 19 });
6. Playwright E2E
Cover only flows that matter for revenue or trust: signup, login, checkout, password reset. E2E is slow — be selective.
1 // playwright.config.ts 2 import { defineConfig, devices } from "@playwright/test"; 3 4 export default defineConfig({ 5 testDir: "./e2e", 6 webServer: { 7 command: "yarn build && yarn start", 8 url: "http://localhost:3000", 9 reuseExistingServer: !process.env.CI, 10 }, 11 use: { baseURL: "http://localhost:3000" }, 12 projects: [ 13 { name: "chromium", use: devices["Desktop Chrome"] }, 14 { name: "mobile", use: devices["iPhone 14"] }, 15 ], 16 }); 17 18 // e2e/login.spec.ts 19 import { test, expect } from "@playwright/test"; 20 21 test("user can log in", async ({ page }) => { 22 await page.goto("/login"); 23 await page.getByLabel("Email").fill("demo@example.com"); 24 await page.getByLabel("Password").fill("password123"); 25 await page.getByRole("button", { name: /sign in/i }).click(); 26 await expect(page).toHaveURL("/dashboard"); 27 await expect(page.getByText("Welcome back")).toBeVisible(); 28 });
7. Scripts
1 // package.json 2 { 3 "scripts": { 4 "test": "jest", 5 "test:watch": "jest --watch", 6 "test:coverage": "jest --coverage", 7 "test:e2e": "playwright test", 8 "test:e2e:ui": "playwright test --ui" 9 } 10 }
8. Rules
- Tests ship in the same diff as the behavior they verify. No "tests later."
- Test through the public API. If you need to import an internal helper to test it, your unit boundary is wrong.
- Use real database fixtures over mocks for migrations and queries — mocked DB tests pass while production breaks.
- Run
test,lint, andbuildin CI on every PR.