Claude Agent Skill · by Wshobson

Javascript Testing Patterns

The javascript-testing-patterns skill provides developers with comprehensive guidance for implementing testing strategies in JavaScript/TypeScript applications

Install
Terminal · npx
$npx skills add https://github.com/wshobson/agents --skill javascript-testing-patterns
Works with Paperclip

How Javascript Testing Patterns fits into a Paperclip company.

Javascript Testing Patterns drops into any Paperclip agent that handles this kind of work. Assign it to a specialist inside a pre-configured PaperclipOrg company and the skill becomes available on every heartbeat — no prompt engineering, no tool wiring.

S
SaaS FactoryPaired

Pre-configured AI company — 18 agents, 18 skills, one-time purchase.

$27$59
Explore pack
Source file
SKILL.md537 lines
Expand
---name: javascript-testing-patternsdescription: 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.tsimport 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.tsimport { 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.tsexport 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.tsimport { 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.tsexport 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.tsimport { 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.tsexport 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.tsimport { describe, it, expect, vi, beforeEach } from "vitest";import { ApiService } from "./api.service"; // Mock fetch globallyglobal.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.tsimport 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.tsimport { 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.tsexport 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.tsimport { 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.tsexport const logger = {  info: (message: string) => console.log(`INFO: ${message}`),  error: (message: string) => console.error(`ERROR: ${message}`),}; // services/order.service.tsimport { 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.tsimport { 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.tsimport { 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, Assert2. **One assertion per test**: Or logically related assertions3. **Descriptive test names**: Should describe what is being tested4. **Use beforeEach/afterEach**: For setup and teardown5. **Mock external dependencies**: Keep tests isolated6. **Test edge cases**: Not just happy paths7. **Avoid implementation details**: Test behavior, not implementation8. **Use test factories**: For consistent test data9. **Keep tests fast**: Mock slow operations10. **Write tests first (TDD)**: When possible11. **Maintain test coverage**: Aim for 80%+ coverage12. **Use TypeScript**: For type-safe tests13. **Test error handling**: Not just success cases14. **Use data-testid sparingly**: Prefer semantic queries15. **Clean up after tests**: Prevent test pollution