chore: restore files unintentionally deleted in 6c61a4e
This commit is contained in:
@@ -0,0 +1,513 @@
|
||||
# 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 (
|
||||
<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: '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(<UserForm onSubmit={onSubmit} />);
|
||||
|
||||
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>): 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(<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
|
||||
|
||||
```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();
|
||||
});
|
||||
```
|
||||
Reference in New Issue
Block a user