# Advanced JavaScript Testing Patterns Advanced patterns for integration testing, frontend component testing, fixtures, snapshot testing, coverage, and common test utilities. ## Integration Testing ### Pattern 1: API Integration Tests ```typescript // tests/integration/user.api.test.ts import request from "supertest"; import { app } from "../../src/app"; import { pool } from "../../src/config/database"; describe("User API Integration Tests", () => { beforeAll(async () => { // Setup test database await pool.query("CREATE TABLE IF NOT EXISTS users (...)"); }); afterAll(async () => { // Cleanup await pool.query("DROP TABLE IF EXISTS users"); await pool.end(); }); beforeEach(async () => { // Clear data before each test await pool.query("TRUNCATE TABLE users CASCADE"); }); describe("POST /api/users", () => { it("should create a new user", async () => { const userData = { name: "John Doe", email: "john@example.com", password: "password123", }; const response = await request(app) .post("/api/users") .send(userData) .expect(201); expect(response.body).toMatchObject({ name: userData.name, email: userData.email, }); expect(response.body).toHaveProperty("id"); expect(response.body).not.toHaveProperty("password"); }); it("should return 400 if email is invalid", async () => { const userData = { name: "John Doe", email: "invalid-email", password: "password123", }; const response = await request(app) .post("/api/users") .send(userData) .expect(400); expect(response.body).toHaveProperty("error"); }); it("should return 409 if email already exists", async () => { const userData = { name: "John Doe", email: "john@example.com", password: "password123", }; await request(app).post("/api/users").send(userData); const response = await request(app) .post("/api/users") .send(userData) .expect(409); expect(response.body.error).toContain("already exists"); }); }); describe("GET /api/users/:id", () => { it("should get user by id", async () => { const createResponse = await request(app).post("/api/users").send({ name: "John Doe", email: "john@example.com", password: "password123", }); const userId = createResponse.body.id; const response = await request(app) .get(`/api/users/${userId}`) .expect(200); expect(response.body).toMatchObject({ id: userId, name: "John Doe", email: "john@example.com", }); }); it("should return 404 if user not found", async () => { await request(app).get("/api/users/999").expect(404); }); }); describe("Authentication", () => { it("should require authentication for protected routes", async () => { await request(app).get("/api/users/me").expect(401); }); it("should allow access with valid token", async () => { // Create user and login await request(app).post("/api/users").send({ name: "John Doe", email: "john@example.com", password: "password123", }); const loginResponse = await request(app).post("/api/auth/login").send({ email: "john@example.com", password: "password123", }); const token = loginResponse.body.token; const response = await request(app) .get("/api/users/me") .set("Authorization", `Bearer ${token}`) .expect(200); expect(response.body.email).toBe("john@example.com"); }); }); }); ``` ### Pattern 2: Database Integration Tests ```typescript // tests/integration/user.repository.test.ts import { describe, it, expect, beforeAll, afterAll, beforeEach } from "vitest"; import { Pool } from "pg"; import { UserRepository } from "../../src/repositories/user.repository"; describe("UserRepository Integration Tests", () => { let pool: Pool; let repository: UserRepository; beforeAll(async () => { pool = new Pool({ host: "localhost", port: 5432, database: "test_db", user: "test_user", password: "test_password", }); repository = new UserRepository(pool); // Create tables await pool.query(` CREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL, email VARCHAR(255) UNIQUE NOT NULL, password VARCHAR(255) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) `); }); afterAll(async () => { await pool.query("DROP TABLE IF EXISTS users"); await pool.end(); }); beforeEach(async () => { await pool.query("TRUNCATE TABLE users CASCADE"); }); it("should create a user", async () => { const user = await repository.create({ name: "John Doe", email: "john@example.com", password: "hashed_password", }); expect(user).toHaveProperty("id"); expect(user.name).toBe("John Doe"); expect(user.email).toBe("john@example.com"); }); it("should find user by email", async () => { await repository.create({ name: "John Doe", email: "john@example.com", password: "hashed_password", }); const user = await repository.findByEmail("john@example.com"); expect(user).toBeTruthy(); expect(user?.name).toBe("John Doe"); }); it("should return null if user not found", async () => { const user = await repository.findByEmail("nonexistent@example.com"); expect(user).toBeNull(); }); }); ``` ## Frontend Testing with Testing Library ### Pattern 1: React Component Testing ```typescript // components/UserForm.tsx import { useState } from 'react'; interface Props { onSubmit: (user: { name: string; email: string }) => void; } export function UserForm({ onSubmit }: Props) { const [name, setName] = useState(''); const [email, setEmail] = useState(''); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); onSubmit({ name, email }); }; return (
setName(e.target.value)} data-testid="name-input" /> setEmail(e.target.value)} data-testid="email-input" />
); } // components/UserForm.test.tsx import { render, screen, fireEvent } from '@testing-library/react'; import { describe, it, expect, vi } from 'vitest'; import { UserForm } from './UserForm'; describe('UserForm', () => { it('should render form inputs', () => { render(); expect(screen.getByPlaceholderText('Name')).toBeInTheDocument(); expect(screen.getByPlaceholderText('Email')).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Submit' })).toBeInTheDocument(); }); it('should update input values', () => { render(); const nameInput = screen.getByTestId('name-input') as HTMLInputElement; const emailInput = screen.getByTestId('email-input') as HTMLInputElement; fireEvent.change(nameInput, { target: { value: 'John Doe' } }); fireEvent.change(emailInput, { target: { value: 'john@example.com' } }); expect(nameInput.value).toBe('John Doe'); expect(emailInput.value).toBe('john@example.com'); }); it('should call onSubmit with form data', () => { const onSubmit = vi.fn(); render(); fireEvent.change(screen.getByTestId('name-input'), { target: { value: 'John Doe' }, }); fireEvent.change(screen.getByTestId('email-input'), { target: { value: 'john@example.com' }, }); fireEvent.click(screen.getByRole('button', { name: 'Submit' })); expect(onSubmit).toHaveBeenCalledWith({ name: 'John Doe', email: 'john@example.com', }); }); }); ``` ### Pattern 2: Testing Hooks ```typescript // hooks/useCounter.ts import { useState, useCallback } from "react"; export function useCounter(initialValue = 0) { const [count, setCount] = useState(initialValue); const increment = useCallback(() => setCount((c) => c + 1), []); const decrement = useCallback(() => setCount((c) => c - 1), []); const reset = useCallback(() => setCount(initialValue), [initialValue]); return { count, increment, decrement, reset }; } // hooks/useCounter.test.ts import { renderHook, act } from "@testing-library/react"; import { describe, it, expect } from "vitest"; import { useCounter } from "./useCounter"; describe("useCounter", () => { it("should initialize with default value", () => { const { result } = renderHook(() => useCounter()); expect(result.current.count).toBe(0); }); it("should initialize with custom value", () => { const { result } = renderHook(() => useCounter(10)); expect(result.current.count).toBe(10); }); it("should increment count", () => { const { result } = renderHook(() => useCounter()); act(() => { result.current.increment(); }); expect(result.current.count).toBe(1); }); it("should decrement count", () => { const { result } = renderHook(() => useCounter(5)); act(() => { result.current.decrement(); }); expect(result.current.count).toBe(4); }); it("should reset to initial value", () => { const { result } = renderHook(() => useCounter(10)); act(() => { result.current.increment(); result.current.increment(); }); expect(result.current.count).toBe(12); act(() => { result.current.reset(); }); expect(result.current.count).toBe(10); }); }); ``` ## Test Fixtures and Factories ```typescript // tests/fixtures/user.fixture.ts import { faker } from "@faker-js/faker"; export function createUserFixture(overrides?: Partial): User { return { id: faker.string.uuid(), name: faker.person.fullName(), email: faker.internet.email(), createdAt: faker.date.past(), ...overrides, }; } export function createUsersFixture(count: number): User[] { return Array.from({ length: count }, () => createUserFixture()); } // Usage in tests import { createUserFixture, createUsersFixture, } from "../fixtures/user.fixture"; describe("UserService", () => { it("should process user", () => { const user = createUserFixture({ name: "John Doe" }); // Use user in test }); it("should handle multiple users", () => { const users = createUsersFixture(10); // Use users in test }); }); ``` ## Snapshot Testing ```typescript // components/UserCard.test.tsx import { render } from '@testing-library/react'; import { describe, it, expect } from 'vitest'; import { UserCard } from './UserCard'; describe('UserCard', () => { it('should match snapshot', () => { const user = { id: '1', name: 'John Doe', email: 'john@example.com', avatar: 'https://example.com/avatar.jpg', }; const { container } = render(); expect(container.firstChild).toMatchSnapshot(); }); it('should match snapshot with loading state', () => { const { container } = render(); expect(container.firstChild).toMatchSnapshot(); }); }); ``` ## Coverage Reports ```typescript // package.json { "scripts": { "test": "vitest", "test:coverage": "vitest --coverage", "test:ui": "vitest --ui" } } ``` ## Common Patterns ### Test Organization ```typescript describe("UserService", () => { describe("createUser", () => { it("should create user successfully", () => {}); it("should throw error if email exists", () => {}); it("should hash password", () => {}); }); describe("updateUser", () => { it("should update user", () => {}); it("should throw error if not found", () => {}); }); }); ``` ### Testing Promises ```typescript // Using async/await it("should fetch user", async () => { const user = await service.fetchUser("1"); expect(user).toBeDefined(); }); // Testing rejections it("should throw error", async () => { await expect(service.fetchUser("invalid")).rejects.toThrow("Not found"); }); ``` ### Testing Timers ```typescript import { vi } from "vitest"; it("should call function after delay", () => { vi.useFakeTimers(); const callback = vi.fn(); setTimeout(callback, 1000); expect(callback).not.toHaveBeenCalled(); vi.advanceTimersByTime(1000); expect(callback).toHaveBeenCalled(); vi.useRealTimers(); }); ```