chore: restore files unintentionally deleted in 6c61a4e
This commit is contained in:
@@ -0,0 +1,537 @@
|
||||
---
|
||||
name: javascript-testing-patterns
|
||||
description: Implement comprehensive testing strategies using Jest, Vitest, and Testing Library for unit tests, integration tests, and end-to-end testing with mocking, fixtures, and test-driven development. Use when writing JavaScript/TypeScript tests, setting up test infrastructure, or implementing TDD/BDD workflows.
|
||||
---
|
||||
|
||||
# JavaScript Testing Patterns
|
||||
|
||||
Comprehensive guide for implementing robust testing strategies in JavaScript/TypeScript applications using modern testing frameworks and best practices.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
- Setting up test infrastructure for new projects
|
||||
- Writing unit tests for functions and classes
|
||||
- Creating integration tests for APIs and services
|
||||
- Implementing end-to-end tests for user flows
|
||||
- Mocking external dependencies and APIs
|
||||
- Testing React, Vue, or other frontend components
|
||||
- Implementing test-driven development (TDD)
|
||||
- Setting up continuous testing in CI/CD pipelines
|
||||
|
||||
## Testing Frameworks
|
||||
|
||||
### Jest - Full-Featured Testing Framework
|
||||
|
||||
**Setup:**
|
||||
|
||||
```typescript
|
||||
// jest.config.ts
|
||||
import type { Config } from "jest";
|
||||
|
||||
const config: Config = {
|
||||
preset: "ts-jest",
|
||||
testEnvironment: "node",
|
||||
roots: ["<rootDir>/src"],
|
||||
testMatch: ["**/__tests__/**/*.ts", "**/?(*.)+(spec|test).ts"],
|
||||
collectCoverageFrom: [
|
||||
"src/**/*.ts",
|
||||
"!src/**/*.d.ts",
|
||||
"!src/**/*.interface.ts",
|
||||
],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
branches: 80,
|
||||
functions: 80,
|
||||
lines: 80,
|
||||
statements: 80,
|
||||
},
|
||||
},
|
||||
setupFilesAfterEnv: ["<rootDir>/src/test/setup.ts"],
|
||||
};
|
||||
|
||||
export default config;
|
||||
```
|
||||
|
||||
### Vitest - Fast, Vite-Native Testing
|
||||
|
||||
**Setup:**
|
||||
|
||||
```typescript
|
||||
// vitest.config.ts
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
environment: "node",
|
||||
coverage: {
|
||||
provider: "v8",
|
||||
reporter: ["text", "json", "html"],
|
||||
exclude: ["**/*.d.ts", "**/*.config.ts", "**/dist/**"],
|
||||
},
|
||||
setupFiles: ["./src/test/setup.ts"],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Unit Testing Patterns
|
||||
|
||||
### Pattern 1: Testing Pure Functions
|
||||
|
||||
```typescript
|
||||
// utils/calculator.ts
|
||||
export function add(a: number, b: number): number {
|
||||
return a + b;
|
||||
}
|
||||
|
||||
export function divide(a: number, b: number): number {
|
||||
if (b === 0) {
|
||||
throw new Error("Division by zero");
|
||||
}
|
||||
return a / b;
|
||||
}
|
||||
|
||||
// utils/calculator.test.ts
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { add, divide } from "./calculator";
|
||||
|
||||
describe("Calculator", () => {
|
||||
describe("add", () => {
|
||||
it("should add two positive numbers", () => {
|
||||
expect(add(2, 3)).toBe(5);
|
||||
});
|
||||
|
||||
it("should add negative numbers", () => {
|
||||
expect(add(-2, -3)).toBe(-5);
|
||||
});
|
||||
|
||||
it("should handle zero", () => {
|
||||
expect(add(0, 5)).toBe(5);
|
||||
expect(add(5, 0)).toBe(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe("divide", () => {
|
||||
it("should divide two numbers", () => {
|
||||
expect(divide(10, 2)).toBe(5);
|
||||
});
|
||||
|
||||
it("should handle decimal results", () => {
|
||||
expect(divide(5, 2)).toBe(2.5);
|
||||
});
|
||||
|
||||
it("should throw error when dividing by zero", () => {
|
||||
expect(() => divide(10, 0)).toThrow("Division by zero");
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Pattern 2: Testing Classes
|
||||
|
||||
```typescript
|
||||
// services/user.service.ts
|
||||
export class UserService {
|
||||
private users: Map<string, User> = new Map();
|
||||
|
||||
create(user: User): User {
|
||||
if (this.users.has(user.id)) {
|
||||
throw new Error("User already exists");
|
||||
}
|
||||
this.users.set(user.id, user);
|
||||
return user;
|
||||
}
|
||||
|
||||
findById(id: string): User | undefined {
|
||||
return this.users.get(id);
|
||||
}
|
||||
|
||||
update(id: string, updates: Partial<User>): User {
|
||||
const user = this.users.get(id);
|
||||
if (!user) {
|
||||
throw new Error("User not found");
|
||||
}
|
||||
const updated = { ...user, ...updates };
|
||||
this.users.set(id, updated);
|
||||
return updated;
|
||||
}
|
||||
|
||||
delete(id: string): boolean {
|
||||
return this.users.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
// services/user.service.test.ts
|
||||
import { describe, it, expect, beforeEach } from "vitest";
|
||||
import { UserService } from "./user.service";
|
||||
|
||||
describe("UserService", () => {
|
||||
let service: UserService;
|
||||
|
||||
beforeEach(() => {
|
||||
service = new UserService();
|
||||
});
|
||||
|
||||
describe("create", () => {
|
||||
it("should create a new user", () => {
|
||||
const user = { id: "1", name: "John", email: "john@example.com" };
|
||||
const created = service.create(user);
|
||||
|
||||
expect(created).toEqual(user);
|
||||
expect(service.findById("1")).toEqual(user);
|
||||
});
|
||||
|
||||
it("should throw error if user already exists", () => {
|
||||
const user = { id: "1", name: "John", email: "john@example.com" };
|
||||
service.create(user);
|
||||
|
||||
expect(() => service.create(user)).toThrow("User already exists");
|
||||
});
|
||||
});
|
||||
|
||||
describe("update", () => {
|
||||
it("should update existing user", () => {
|
||||
const user = { id: "1", name: "John", email: "john@example.com" };
|
||||
service.create(user);
|
||||
|
||||
const updated = service.update("1", { name: "Jane" });
|
||||
|
||||
expect(updated.name).toBe("Jane");
|
||||
expect(updated.email).toBe("john@example.com");
|
||||
});
|
||||
|
||||
it("should throw error if user not found", () => {
|
||||
expect(() => service.update("999", { name: "Jane" })).toThrow(
|
||||
"User not found",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Pattern 3: Testing Async Functions
|
||||
|
||||
```typescript
|
||||
// services/api.service.ts
|
||||
export class ApiService {
|
||||
async fetchUser(id: string): Promise<User> {
|
||||
const response = await fetch(`https://api.example.com/users/${id}`);
|
||||
if (!response.ok) {
|
||||
throw new Error("User not found");
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async createUser(user: CreateUserDTO): Promise<User> {
|
||||
const response = await fetch("https://api.example.com/users", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(user),
|
||||
});
|
||||
return response.json();
|
||||
}
|
||||
}
|
||||
|
||||
// services/api.service.test.ts
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { ApiService } from "./api.service";
|
||||
|
||||
// Mock fetch globally
|
||||
global.fetch = vi.fn();
|
||||
|
||||
describe("ApiService", () => {
|
||||
let service: ApiService;
|
||||
|
||||
beforeEach(() => {
|
||||
service = new ApiService();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("fetchUser", () => {
|
||||
it("should fetch user successfully", async () => {
|
||||
const mockUser = { id: "1", name: "John", email: "john@example.com" };
|
||||
|
||||
(fetch as any).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => mockUser,
|
||||
});
|
||||
|
||||
const user = await service.fetchUser("1");
|
||||
|
||||
expect(user).toEqual(mockUser);
|
||||
expect(fetch).toHaveBeenCalledWith("https://api.example.com/users/1");
|
||||
});
|
||||
|
||||
it("should throw error if user not found", async () => {
|
||||
(fetch as any).mockResolvedValueOnce({
|
||||
ok: false,
|
||||
});
|
||||
|
||||
await expect(service.fetchUser("999")).rejects.toThrow("User not found");
|
||||
});
|
||||
});
|
||||
|
||||
describe("createUser", () => {
|
||||
it("should create user successfully", async () => {
|
||||
const newUser = { name: "John", email: "john@example.com" };
|
||||
const createdUser = { id: "1", ...newUser };
|
||||
|
||||
(fetch as any).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => createdUser,
|
||||
});
|
||||
|
||||
const user = await service.createUser(newUser);
|
||||
|
||||
expect(user).toEqual(createdUser);
|
||||
expect(fetch).toHaveBeenCalledWith(
|
||||
"https://api.example.com/users",
|
||||
expect.objectContaining({
|
||||
method: "POST",
|
||||
body: JSON.stringify(newUser),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Mocking Patterns
|
||||
|
||||
### Pattern 1: Mocking Modules
|
||||
|
||||
```typescript
|
||||
// services/email.service.ts
|
||||
import nodemailer from "nodemailer";
|
||||
|
||||
export class EmailService {
|
||||
private transporter = nodemailer.createTransport({
|
||||
host: process.env.SMTP_HOST,
|
||||
port: 587,
|
||||
auth: {
|
||||
user: process.env.SMTP_USER,
|
||||
pass: process.env.SMTP_PASS,
|
||||
},
|
||||
});
|
||||
|
||||
async sendEmail(to: string, subject: string, html: string) {
|
||||
await this.transporter.sendMail({
|
||||
from: process.env.EMAIL_FROM,
|
||||
to,
|
||||
subject,
|
||||
html,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// services/email.service.test.ts
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { EmailService } from "./email.service";
|
||||
|
||||
vi.mock("nodemailer", () => ({
|
||||
default: {
|
||||
createTransport: vi.fn(() => ({
|
||||
sendMail: vi.fn().mockResolvedValue({ messageId: "123" }),
|
||||
})),
|
||||
},
|
||||
}));
|
||||
|
||||
describe("EmailService", () => {
|
||||
let service: EmailService;
|
||||
|
||||
beforeEach(() => {
|
||||
service = new EmailService();
|
||||
});
|
||||
|
||||
it("should send email successfully", async () => {
|
||||
await service.sendEmail(
|
||||
"test@example.com",
|
||||
"Test Subject",
|
||||
"<p>Test Body</p>",
|
||||
);
|
||||
|
||||
expect(service["transporter"].sendMail).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
to: "test@example.com",
|
||||
subject: "Test Subject",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Pattern 2: Dependency Injection for Testing
|
||||
|
||||
```typescript
|
||||
// services/user.service.ts
|
||||
export interface IUserRepository {
|
||||
findById(id: string): Promise<User | null>;
|
||||
create(user: User): Promise<User>;
|
||||
}
|
||||
|
||||
export class UserService {
|
||||
constructor(private userRepository: IUserRepository) {}
|
||||
|
||||
async getUser(id: string): Promise<User> {
|
||||
const user = await this.userRepository.findById(id);
|
||||
if (!user) {
|
||||
throw new Error("User not found");
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
async createUser(userData: CreateUserDTO): Promise<User> {
|
||||
// Business logic here
|
||||
const user = { id: generateId(), ...userData };
|
||||
return this.userRepository.create(user);
|
||||
}
|
||||
}
|
||||
|
||||
// services/user.service.test.ts
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { UserService, IUserRepository } from "./user.service";
|
||||
|
||||
describe("UserService", () => {
|
||||
let service: UserService;
|
||||
let mockRepository: IUserRepository;
|
||||
|
||||
beforeEach(() => {
|
||||
mockRepository = {
|
||||
findById: vi.fn(),
|
||||
create: vi.fn(),
|
||||
};
|
||||
service = new UserService(mockRepository);
|
||||
});
|
||||
|
||||
describe("getUser", () => {
|
||||
it("should return user if found", async () => {
|
||||
const mockUser = { id: "1", name: "John", email: "john@example.com" };
|
||||
vi.mocked(mockRepository.findById).mockResolvedValue(mockUser);
|
||||
|
||||
const user = await service.getUser("1");
|
||||
|
||||
expect(user).toEqual(mockUser);
|
||||
expect(mockRepository.findById).toHaveBeenCalledWith("1");
|
||||
});
|
||||
|
||||
it("should throw error if user not found", async () => {
|
||||
vi.mocked(mockRepository.findById).mockResolvedValue(null);
|
||||
|
||||
await expect(service.getUser("999")).rejects.toThrow("User not found");
|
||||
});
|
||||
});
|
||||
|
||||
describe("createUser", () => {
|
||||
it("should create user successfully", async () => {
|
||||
const userData = { name: "John", email: "john@example.com" };
|
||||
const createdUser = { id: "1", ...userData };
|
||||
|
||||
vi.mocked(mockRepository.create).mockResolvedValue(createdUser);
|
||||
|
||||
const user = await service.createUser(userData);
|
||||
|
||||
expect(user).toEqual(createdUser);
|
||||
expect(mockRepository.create).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Pattern 3: Spying on Functions
|
||||
|
||||
```typescript
|
||||
// utils/logger.ts
|
||||
export const logger = {
|
||||
info: (message: string) => console.log(`INFO: ${message}`),
|
||||
error: (message: string) => console.error(`ERROR: ${message}`),
|
||||
};
|
||||
|
||||
// services/order.service.ts
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
export class OrderService {
|
||||
async processOrder(orderId: string): Promise<void> {
|
||||
logger.info(`Processing order ${orderId}`);
|
||||
// Process order logic
|
||||
logger.info(`Order ${orderId} processed successfully`);
|
||||
}
|
||||
}
|
||||
|
||||
// services/order.service.test.ts
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { OrderService } from "./order.service";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
describe("OrderService", () => {
|
||||
let service: OrderService;
|
||||
let loggerSpy: any;
|
||||
|
||||
beforeEach(() => {
|
||||
service = new OrderService();
|
||||
loggerSpy = vi.spyOn(logger, "info");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
loggerSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("should log order processing", async () => {
|
||||
await service.processOrder("123");
|
||||
|
||||
expect(loggerSpy).toHaveBeenCalledWith("Processing order 123");
|
||||
expect(loggerSpy).toHaveBeenCalledWith("Order 123 processed successfully");
|
||||
expect(loggerSpy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Integration Testing
|
||||
|
||||
Integration tests verify real database operations and HTTP endpoints using `supertest` and a test database instance. Always truncate tables in `beforeEach` and tear down in `afterAll`.
|
||||
|
||||
For full API integration test examples (supertest + PostgreSQL) and database repository integration tests, see [references/advanced-testing-patterns.md](references/advanced-testing-patterns.md).
|
||||
|
||||
## Frontend Testing with Testing Library
|
||||
|
||||
Test React components by rendering them and querying by role, placeholder, or test ID. Test hooks with `renderHook` + `act`. Prefer semantic queries (`getByRole`, `getByPlaceholderText`) over `data-testid`.
|
||||
|
||||
For complete React component test examples (UserForm, hooks with `renderHook`/`act`), see [references/advanced-testing-patterns.md](references/advanced-testing-patterns.md).
|
||||
|
||||
## Test Fixtures and Factories
|
||||
|
||||
Use `@faker-js/faker` to generate realistic test data factories. Factories accept optional `overrides` so tests can set only the fields they care about:
|
||||
|
||||
```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,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
For snapshot testing, coverage configuration, test organization patterns, promise testing, and timer mocking, see [references/advanced-testing-patterns.md](references/advanced-testing-patterns.md).
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Follow AAA Pattern**: Arrange, Act, Assert
|
||||
2. **One assertion per test**: Or logically related assertions
|
||||
3. **Descriptive test names**: Should describe what is being tested
|
||||
4. **Use beforeEach/afterEach**: For setup and teardown
|
||||
5. **Mock external dependencies**: Keep tests isolated
|
||||
6. **Test edge cases**: Not just happy paths
|
||||
7. **Avoid implementation details**: Test behavior, not implementation
|
||||
8. **Use test factories**: For consistent test data
|
||||
9. **Keep tests fast**: Mock slow operations
|
||||
10. **Write tests first (TDD)**: When possible
|
||||
11. **Maintain test coverage**: Aim for 80%+ coverage
|
||||
12. **Use TypeScript**: For type-safe tests
|
||||
13. **Test error handling**: Not just success cases
|
||||
14. **Use data-testid sparingly**: Prefer semantic queries
|
||||
15. **Clean up after tests**: Prevent test pollution
|
||||
Reference in New Issue
Block a user