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
// 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: "[email protected]",
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: "[email protected]",
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: "[email protected]",
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: "[email protected]",
});
});
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: "[email protected]",
password: "password123",
});
const loginResponse = await request(app).post("/api/auth/login").send({
email: "[email protected]",
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("[email protected]");
});
});
});Pattern 2: Database Integration Tests
// 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: "[email protected]",
password: "hashed_password",
});
expect(user).toHaveProperty("id");
expect(user.name).toBe("John Doe");
expect(user.email).toBe("[email protected]");
});
it("should find user by email", async () => {
await repository.create({
name: "John Doe",
email: "[email protected]",
password: "hashed_password",
});
const user = await repository.findByEmail("[email protected]");
expect(user).toBeTruthy();
expect(user?.name).toBe("John Doe");
});
it("should return null if user not found", async () => {
const user = await repository.findByEmail("[email protected]");
expect(user).toBeNull();
});
});Frontend Testing with Testing Library
Pattern 1: React Component Testing
// 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 (
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="Name"
value={name}
onChange={(e) => setName(e.target.value)}
data-testid="name-input"
/>
<input
type="email"
placeholder="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
data-testid="email-input"
/>
<button type="submit">Submit</button>
</form>
);
}
// 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(<UserForm onSubmit={vi.fn()} />);
expect(screen.getByPlaceholderText('Name')).toBeInTheDocument();
expect(screen.getByPlaceholderText('Email')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Submit' })).toBeInTheDocument();
});
it('should update input values', () => {
render(<UserForm onSubmit={vi.fn()} />);
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: '[email protected]' } });
expect(nameInput.value).toBe('John Doe');
expect(emailInput.value).toBe('[email protected]');
});
it('should call onSubmit with form data', () => {
const onSubmit = vi.fn();
render(<UserForm onSubmit={onSubmit} />);
fireEvent.change(screen.getByTestId('name-input'), {
target: { value: 'John Doe' },
});
fireEvent.change(screen.getByTestId('email-input'), {
target: { value: '[email protected]' },
});
fireEvent.click(screen.getByRole('button', { name: 'Submit' }));
expect(onSubmit).toHaveBeenCalledWith({
name: 'John Doe',
email: '[email protected]',
});
});
});Pattern 2: Testing Hooks
// 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
// tests/fixtures/user.fixture.ts
import { faker } from "@faker-js/faker";
export function createUserFixture(overrides?: Partial<User>): 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
// 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: '[email protected]',
avatar: 'https://example.com/avatar.jpg',
};
const { container } = render(<UserCard user={user} />);
expect(container.firstChild).toMatchSnapshot();
});
it('should match snapshot with loading state', () => {
const { container } = render(<UserCard loading />);
expect(container.firstChild).toMatchSnapshot();
});
});Coverage Reports
// package.json
{
"scripts": {
"test": "vitest",
"test:coverage": "vitest --coverage",
"test:ui": "vitest --ui"
}
}Common Patterns
Test Organization
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
// 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
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();
});