javascript-testing-patterns

Comprehensive testing strategies for JavaScript/TypeScript using Jest, Vitest, and Testing Library. Covers unit testing, integration testing, and component testing with patterns for pure functions, classes, async code, and React hooks Includes mocking strategies: module mocking, dependency injection, and spying on functions for isolated test execution Provides API and database integration test examples with real request/response handling and transaction cleanup Supports snapshot testing, test fixtures, coverage reporting, and best practices like the AAA pattern and TDD workflows

INSTALLATION
npx skills add https://github.com/wshobson/agents --skill javascript-testing-patterns
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

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:

// 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:

// 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

// 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

// 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

// 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

// 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

// 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

// 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.

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:

// 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.

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
BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card