| 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:
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:
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
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;
}
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((-, -)).(-);
});
(, {
((, )).();
((, )).();
});
});
(, {
(, {
((, )).();
});
(, {
((, )).();
});
(, {
( (, )).();
});
});
});
Pattern 2: Testing Classes
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();
}
updated = { ...user, ...updates };
..(id, updated);
updated;
}
(: ): {
..(id);
}
}
{ describe, it, expect, beforeEach } ;
{ } ;
(, {
: ;
( {
service = ();
});
(, {
(, {
user = { : , : , : };
created = service.(user);
(created).(user);
(service.()).(user);
});
(, {
user = { : , : , : };
service.(user);
( service.(user)).();
});
});
(, {
(, {
user = { : , : , : };
service.(user);
updated = service.(, { : });
(updated.).();
(updated.).();
});
(, {
( service.(, { : })).(
,
);
});
});
});
Pattern 3: Testing Async Functions
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();
}
}
import { describe, it, expect, vi, beforeEach } from "vitest";
import { ApiService } from ;
. = vi.();
(, {
: ;
( {
service = ();
vi.();
});
(, {
(, () => {
mockUser = { : , : , : };
(fetch ).({
: ,
: () => mockUser,
});
user = service.();
(user).(mockUser);
(fetch).();
});
(, () => {
(fetch ).({
: ,
});
(service.())..();
});
});
(, {
(, () => {
newUser = { : , : };
createdUser = { : , ...newUser };
(fetch ).({
: ,
: () => createdUser,
});
user = service.(newUser);
(user).(createdUser);
(fetch).(
,
expect.({
: ,
: .(newUser),
}),
);
});
});
});
Mocking Patterns
Pattern 1: Mocking Modules
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,
});
}
}
import { describe, it, expect, vi, beforeEach } from "vitest";
import { EmailService } from "./email.service";
vi.mock("nodemailer", () => ({
default: {
createTransport: vi.fn( ({
: vi.().({ : }),
})),
},
}));
(, {
: ;
( {
service = ();
});
(, () => {
service.(
,
,
,
);
(service[].).(
expect.({
: ,
: ,
}),
);
});
});
Pattern 2: Dependency Injection for Testing
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> {
const user = { id: generateId(), ...userData };
return this..(user);
}
}
{ describe, it, expect, vi, beforeEach } ;
{ , } ;
(, {
: ;
: ;
( {
mockRepository = {
: vi.(),
: vi.(),
};
service = (mockRepository);
});
(, {
(, () => {
mockUser = { : , : , : };
vi.(mockRepository.).(mockUser);
user = service.();
(user).(mockUser);
(mockRepository.).();
});
(, () => {
vi.(mockRepository.).();
(service.())..();
});
});
(, {
(, () => {
userData = { : , : };
createdUser = { : , ...userData };
vi.(mockRepository.).(createdUser);
user = service.(userData);
(user).(createdUser);
(mockRepository.).();
});
});
});
Pattern 3: Spying on Functions
export const logger = {
info: (message: string) => console.log(`INFO: ${message}`),
error: (message: string) => console.error(`ERROR: ${message}`),
};
import { logger } from "../utils/logger";
export class OrderService {
async processOrder(orderId: string): Promise<void> {
logger.info(`Processing order ${orderId}`);
logger.info(`Order ${orderId} processed successfully`);
}
}
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { OrderService } from "./order.service";
import { logger } from "../utils/logger";
(, {
: ;
: ;
( {
service = ();
loggerSpy = vi.(logger, );
});
( {
loggerSpy.();
});
(, () => {
service.();
(loggerSpy).();
(loggerSpy).();
(loggerSpy).();
});
});
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.
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.
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:
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.
Best Practices
- Follow AAA Pattern: Arrange, Act, Assert
- One assertion per test: Or logically related assertions
- Descriptive test names: Should describe what is being tested
- Use beforeEach/afterEach: For setup and teardown
- Mock external dependencies: Keep tests isolated
- Test edge cases: Not just happy paths
- Avoid implementation details: Test behavior, not implementation
- Use test factories: For consistent test data
- Keep tests fast: Mock slow operations
- Write tests first (TDD): When possible
- Maintain test coverage: Aim for 80%+ coverage
- Use TypeScript: For type-safe tests
- Test error handling: Not just success cases
- Use data-testid sparingly: Prefer semantic queries
- Clean up after tests: Prevent test pollution