Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Write effective JavaScript tests with Jest/Vitest, mocking strategies, async testing, and coverage best practices.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
SKILL.md
1---2name: javascript-testing-patterns3description: 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.4---56# JavaScript Testing Patterns78Comprehensive guide for implementing robust testing strategies in JavaScript/TypeScript applications using modern testing frameworks and best practices.910## When to Use This Skill1112- Setting up test infrastructure for new projects13- Writing unit tests for functions and classes14- Creating integration tests for APIs and services15- Implementing end-to-end tests for user flows16- Mocking external dependencies and APIs17- Testing React, Vue, or other frontend components18- Implementing test-driven development (TDD)19- Setting up continuous testing in CI/CD pipelines2021## Testing Frameworks2223### Jest - Full-Featured Testing Framework2425**Setup:**2627```typescript28// jest.config.ts29import type { Config } from "jest";3031const config: Config = {32preset: "ts-jest",33testEnvironment: "node",34roots: ["<rootDir>/src"],35testMatch: ["**/__tests__/**/*.ts", "**/?(*.)+(spec|test).ts"],36collectCoverageFrom: [37"src/**/*.ts",38"!src/**/*.d.ts",39"!src/**/*.interface.ts",40],41coverageThreshold: {42global: {43branches: 80,44functions: 80,45lines: 80,46statements: 80,47},48},49setupFilesAfterEnv: ["<rootDir>/src/test/setup.ts"],50};5152export default config;53```5455### Vitest - Fast, Vite-Native Testing5657**Setup:**5859```typescript60// vitest.config.ts61import { defineConfig } from "vitest/config";6263export default defineConfig({64test: {65globals: true,66environment: "node",67coverage: {68provider: "v8",69reporter: ["text", "json", "html"],70exclude: ["**/*.d.ts", "**/*.config.ts", "**/dist/**"],71},72setupFiles: ["./src/test/setup.ts"],73},74});75```7677## Unit Testing Patterns7879### Pattern 1: Testing Pure Functions8081```typescript82// utils/calculator.ts83export function add(a: number, b: number): number {84return a + b;85}8687export function divide(a: number, b: number): number {88if (b === 0) {89throw new Error("Division by zero");90}91return a / b;92}9394// utils/calculator.test.ts95import { describe, it, expect } from "vitest";96import { add, divide } from "./calculator";9798describe("Calculator", () => {99describe("add", () => {100it("should add two positive numbers", () => {101expect(add(2, 3)).toBe(5);102});103104it("should add negative numbers", () => {105expect(add(-2, -3)).toBe(-5);106});107108it("should handle zero", () => {109expect(add(0, 5)).toBe(5);110expect(add(5, 0)).toBe(5);111});112});113114describe("divide", () => {115it("should divide two numbers", () => {116expect(divide(10, 2)).toBe(5);117});118119it("should handle decimal results", () => {120expect(divide(5, 2)).toBe(2.5);121});122123it("should throw error when dividing by zero", () => {124expect(() => divide(10, 0)).toThrow("Division by zero");125});126});127});128```129130### Pattern 2: Testing Classes131132```typescript133// services/user.service.ts134export class UserService {135private users: Map<string, User> = new Map();136137create(user: User): User {138if (this.users.has(user.id)) {139throw new Error("User already exists");140}141this.users.set(user.id, user);142return user;143}144145findById(id: string): User | undefined {146return this.users.get(id);147}148149update(id: string, updates: Partial<User>): User {150const user = this.users.get(id);151if (!user) {152throw new Error("User not found");153}154const updated = { ...user, ...updates };155this.users.set(id, updated);156return updated;157}158159delete(id: string): boolean {160return this.users.delete(id);161}162}163164// services/user.service.test.ts165import { describe, it, expect, beforeEach } from "vitest";166import { UserService } from "./user.service";167168describe("UserService", () => {169let service: UserService;170171beforeEach(() => {172service = new UserService();173});174175describe("create", () => {176it("should create a new user", () => {177const user = { id: "1", name: "John", email: "[email protected]" };178const created = service.create(user);179180expect(created).toEqual(user);181expect(service.findById("1")).toEqual(user);182});183184it("should throw error if user already exists", () => {185const user = { id: "1", name: "John", email: "[email protected]" };186service.create(user);187188expect(() => service.create(user)).toThrow("User already exists");189});190});191192describe("update", () => {193it("should update existing user", () => {194const user = { id: "1", name: "John", email: "[email protected]" };195service.create(user);196197const updated = service.update("1", { name: "Jane" });198199expect(updated.name).toBe("Jane");200expect(updated.email).toBe("[email protected]");201});202203it("should throw error if user not found", () => {204expect(() => service.update("999", { name: "Jane" })).toThrow(205"User not found",206);207});208});209});210```211212### Pattern 3: Testing Async Functions213214```typescript215// services/api.service.ts216export class ApiService {217async fetchUser(id: string): Promise<User> {218const response = await fetch(`https://api.example.com/users/${id}`);219if (!response.ok) {220throw new Error("User not found");221}222return response.json();223}224225async createUser(user: CreateUserDTO): Promise<User> {226const response = await fetch("https://api.example.com/users", {227method: "POST",228headers: { "Content-Type": "application/json" },229body: JSON.stringify(user),230});231return response.json();232}233}234235// services/api.service.test.ts236import { describe, it, expect, vi, beforeEach } from "vitest";237import { ApiService } from "./api.service";238239// Mock fetch globally240global.fetch = vi.fn();241242describe("ApiService", () => {243let service: ApiService;244245beforeEach(() => {246service = new ApiService();247vi.clearAllMocks();248});249250describe("fetchUser", () => {251it("should fetch user successfully", async () => {252const mockUser = { id: "1", name: "John", email: "[email protected]" };253254(fetch as any).mockResolvedValueOnce({255ok: true,256json: async () => mockUser,257});258259const user = await service.fetchUser("1");260261expect(user).toEqual(mockUser);262expect(fetch).toHaveBeenCalledWith("https://api.example.com/users/1");263});264265it("should throw error if user not found", async () => {266(fetch as any).mockResolvedValueOnce({267ok: false,268});269270await expect(service.fetchUser("999")).rejects.toThrow("User not found");271});272});273274describe("createUser", () => {275it("should create user successfully", async () => {276const newUser = { name: "John", email: "[email protected]" };277const createdUser = { id: "1", ...newUser };278279(fetch as any).mockResolvedValueOnce({280ok: true,281json: async () => createdUser,282});283284const user = await service.createUser(newUser);285286expect(user).toEqual(createdUser);287expect(fetch).toHaveBeenCalledWith(288"https://api.example.com/users",289expect.objectContaining({290method: "POST",291body: JSON.stringify(newUser),292}),293);294});295});296});297```298299## Mocking Patterns300301### Pattern 1: Mocking Modules302303```typescript304// services/email.service.ts305import nodemailer from "nodemailer";306307export class EmailService {308private transporter = nodemailer.createTransport({309host: process.env.SMTP_HOST,310port: 587,311auth: {312user: process.env.SMTP_USER,313pass: process.env.SMTP_PASS,314},315});316317async sendEmail(to: string, subject: string, html: string) {318await this.transporter.sendMail({319from: process.env.EMAIL_FROM,320to,321subject,322html,323});324}325}326327// services/email.service.test.ts328import { describe, it, expect, vi, beforeEach } from "vitest";329import { EmailService } from "./email.service";330331vi.mock("nodemailer", () => ({332default: {333createTransport: vi.fn(() => ({334sendMail: vi.fn().mockResolvedValue({ messageId: "123" }),335})),336},337}));338339describe("EmailService", () => {340let service: EmailService;341342beforeEach(() => {343service = new EmailService();344});345346it("should send email successfully", async () => {347await service.sendEmail(348"[email protected]",349"Test Subject",350"<p>Test Body</p>",351);352353expect(service["transporter"].sendMail).toHaveBeenCalledWith(354expect.objectContaining({355to: "[email protected]",356subject: "Test Subject",357}),358);359});360});361```362363### Pattern 2: Dependency Injection for Testing364365```typescript366// services/user.service.ts367export interface IUserRepository {368findById(id: string): Promise<User | null>;369create(user: User): Promise<User>;370}371372export class UserService {373constructor(private userRepository: IUserRepository) {}374375async getUser(id: string): Promise<User> {376const user = await this.userRepository.findById(id);377if (!user) {378throw new Error("User not found");379}380return user;381}382383async createUser(userData: CreateUserDTO): Promise<User> {384// Business logic here385const user = { id: generateId(), ...userData };386return this.userRepository.create(user);387}388}389390// services/user.service.test.ts391import { describe, it, expect, vi, beforeEach } from "vitest";392import { UserService, IUserRepository } from "./user.service";393394describe("UserService", () => {395let service: UserService;396let mockRepository: IUserRepository;397398beforeEach(() => {399mockRepository = {400findById: vi.fn(),401create: vi.fn(),402};403service = new UserService(mockRepository);404});405406describe("getUser", () => {407it("should return user if found", async () => {408const mockUser = { id: "1", name: "John", email: "[email protected]" };409vi.mocked(mockRepository.findById).mockResolvedValue(mockUser);410411const user = await service.getUser("1");412413expect(user).toEqual(mockUser);414expect(mockRepository.findById).toHaveBeenCalledWith("1");415});416417it("should throw error if user not found", async () => {418vi.mocked(mockRepository.findById).mockResolvedValue(null);419420await expect(service.getUser("999")).rejects.toThrow("User not found");421});422});423424describe("createUser", () => {425it("should create user successfully", async () => {426const userData = { name: "John", email: "[email protected]" };427const createdUser = { id: "1", ...userData };428429vi.mocked(mockRepository.create).mockResolvedValue(createdUser);430431const user = await service.createUser(userData);432433expect(user).toEqual(createdUser);434expect(mockRepository.create).toHaveBeenCalled();435});436});437});438```439440### Pattern 3: Spying on Functions441442```typescript443// utils/logger.ts444export const logger = {445info: (message: string) => console.log(`INFO: ${message}`),446error: (message: string) => console.error(`ERROR: ${message}`),447};448449// services/order.service.ts450import { logger } from "../utils/logger";451452export class OrderService {453async processOrder(orderId: string): Promise<void> {454logger.info(`Processing order ${orderId}`);455// Process order logic456logger.info(`Order ${orderId} processed successfully`);457}458}459460// services/order.service.test.ts461import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";462import { OrderService } from "./order.service";463import { logger } from "../utils/logger";464465describe("OrderService", () => {466let service: OrderService;467let loggerSpy: any;468469beforeEach(() => {470service = new OrderService();471loggerSpy = vi.spyOn(logger, "info");472});473474afterEach(() => {475loggerSpy.mockRestore();476});477478it("should log order processing", async () => {479await service.processOrder("123");480481expect(loggerSpy).toHaveBeenCalledWith("Processing order 123");482expect(loggerSpy).toHaveBeenCalledWith("Order 123 processed successfully");483expect(loggerSpy).toHaveBeenCalledTimes(2);484});485});486```487488## Integration Testing489490Integration 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`.491492For full API integration test examples (supertest + PostgreSQL) and database repository integration tests, see [references/advanced-testing-patterns.md](references/advanced-testing-patterns.md).493494## Frontend Testing with Testing Library495496Test 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`.497498For complete React component test examples (UserForm, hooks with `renderHook`/`act`), see [references/advanced-testing-patterns.md](references/advanced-testing-patterns.md).499500## Test Fixtures and Factories501502Use `@faker-js/faker` to generate realistic test data factories. Factories accept optional `overrides` so tests can set only the fields they care about:503504```typescript505// tests/fixtures/user.fixture.ts506import { faker } from "@faker-js/faker";507508export function createUserFixture(overrides?: Partial<User>): User {509return {510id: faker.string.uuid(),511name: faker.person.fullName(),512email: faker.internet.email(),513createdAt: faker.date.past(),514...overrides,515};516}517```518519For snapshot testing, coverage configuration, test organization patterns, promise testing, and timer mocking, see [references/advanced-testing-patterns.md](references/advanced-testing-patterns.md).520521## Best Practices5225231. **Follow AAA Pattern**: Arrange, Act, Assert5242. **One assertion per test**: Or logically related assertions5253. **Descriptive test names**: Should describe what is being tested5264. **Use beforeEach/afterEach**: For setup and teardown5275. **Mock external dependencies**: Keep tests isolated5286. **Test edge cases**: Not just happy paths5297. **Avoid implementation details**: Test behavior, not implementation5308. **Use test factories**: For consistent test data5319. **Keep tests fast**: Mock slow operations53210. **Write tests first (TDD)**: When possible53311. **Maintain test coverage**: Aim for 80%+ coverage53412. **Use TypeScript**: For type-safe tests53513. **Test error handling**: Not just success cases53614. **Use data-testid sparingly**: Prefer semantic queries53715. **Clean up after tests**: Prevent test pollution538