Claude Cursor Skill

javascript-testing-patterns

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 imple

LLM Mart · 0 points · 10 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download wshobson-agents-plugins_javascript-typescript_skills_javascript-testing-patterns-554237f.zip · 7 KB
Part of wshobson/agents — 170 skills

Install

skills CLI npx skills add https://github.com/wshobson/agents/tree/main/plugins/javascript-typescript/skills/javascript-testing-patterns
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install wshobson-agents@llmmart
Git git clone https://github.com/wshobson/agents.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole wshobson/agents collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

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

  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
Files (agents)
  • references
    • advanced-testing-patterns.md 12.5 KB
      # Advanced JavaScript Testing Patterns
      
      Advanced patterns for integration testing, frontend component testing, fixtures, snapshot testing, coverage, and common test utilities.
      
      ## Integration Testing
      
      ### Pattern 1: API Integration Tests
      
      ```typescript
      // tests/integration/user.api.test.ts
      import request from "supertest";
      import { app } from "../../src/app";
      import { pool } from "../../src/config/database";
      
      describe("User API Integration Tests", () => {
        beforeAll(async () => {
          // Setup test database
          await pool.query("CREATE TABLE IF NOT EXISTS users (...)");
        });
      
        afterAll(async () => {
          // Cleanup
          await pool.query("DROP TABLE IF EXISTS users");
          await pool.end();
        });
      
        beforeEach(async () => {
          // Clear data before each test
          await pool.query("TRUNCATE TABLE users CASCADE");
        });
      
        describe("POST /api/users", () => {
          it("should create a new user", async () => {
            const userData = {
              name: "John Doe",
              email: "john@example.com",
              password: "password123",
            };
      
            const response = await request(app)
              .post("/api/users")
              .send(userData)
              .expect(201);
      
            expect(response.body).toMatchObject({
              name: userData.name,
              email: userData.email,
            });
            expect(response.body).toHaveProperty("id");
            expect(response.body).not.toHaveProperty("password");
          });
      
          it("should return 400 if email is invalid", async () => {
            const userData = {
              name: "John Doe",
              email: "invalid-email",
              password: "password123",
            };
      
            const response = await request(app)
              .post("/api/users")
              .send(userData)
              .expect(400);
      
            expect(response.body).toHaveProperty("error");
          });
      
          it("should return 409 if email already exists", async () => {
            const userData = {
              name: "John Doe",
              email: "john@example.com",
              password: "password123",
            };
      
            await request(app).post("/api/users").send(userData);
      
            const response = await request(app)
              .post("/api/users")
              .send(userData)
              .expect(409);
      
            expect(response.body.error).toContain("already exists");
          });
        });
      
        describe("GET /api/users/:id", () => {
          it("should get user by id", async () => {
            const createResponse = await request(app).post("/api/users").send({
              name: "John Doe",
              email: "john@example.com",
              password: "password123",
            });
      
            const userId = createResponse.body.id;
      
            const response = await request(app)
              .get(`/api/users/${userId}`)
              .expect(200);
      
            expect(response.body).toMatchObject({
              id: userId,
              name: "John Doe",
              email: "john@example.com",
            });
          });
      
          it("should return 404 if user not found", async () => {
            await request(app).get("/api/users/999").expect(404);
          });
        });
      
        describe("Authentication", () => {
          it("should require authentication for protected routes", async () => {
            await request(app).get("/api/users/me").expect(401);
          });
      
          it("should allow access with valid token", async () => {
            // Create user and login
            await request(app).post("/api/users").send({
              name: "John Doe",
              email: "john@example.com",
              password: "password123",
            });
      
            const loginResponse = await request(app).post("/api/auth/login").send({
              email: "john@example.com",
              password: "password123",
            });
      
            const token = loginResponse.body.token;
      
            const response = await request(app)
              .get("/api/users/me")
              .set("Authorization", `Bearer ${token}`)
              .expect(200);
      
            expect(response.body.email).toBe("john@example.com");
          });
        });
      });
      ```
      
      ### Pattern 2: Database Integration Tests
      
      ```typescript
      // tests/integration/user.repository.test.ts
      import { describe, it, expect, beforeAll, afterAll, beforeEach } from "vitest";
      import { Pool } from "pg";
      import { UserRepository } from "../../src/repositories/user.repository";
      
      describe("UserRepository Integration Tests", () => {
        let pool: Pool;
        let repository: UserRepository;
      
        beforeAll(async () => {
          pool = new Pool({
            host: "localhost",
            port: 5432,
            database: "test_db",
            user: "test_user",
            password: "test_password",
          });
      
          repository = new UserRepository(pool);
      
          // Create tables
          await pool.query(`
            CREATE TABLE IF NOT EXISTS users (
              id SERIAL PRIMARY KEY,
              name VARCHAR(255) NOT NULL,
              email VARCHAR(255) UNIQUE NOT NULL,
              password VARCHAR(255) NOT NULL,
              created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
            )
          `);
        });
      
        afterAll(async () => {
          await pool.query("DROP TABLE IF EXISTS users");
          await pool.end();
        });
      
        beforeEach(async () => {
          await pool.query("TRUNCATE TABLE users CASCADE");
        });
      
        it("should create a user", async () => {
          const user = await repository.create({
            name: "John Doe",
            email: "john@example.com",
            password: "hashed_password",
          });
      
          expect(user).toHaveProperty("id");
          expect(user.name).toBe("John Doe");
          expect(user.email).toBe("john@example.com");
        });
      
        it("should find user by email", async () => {
          await repository.create({
            name: "John Doe",
            email: "john@example.com",
            password: "hashed_password",
          });
      
          const user = await repository.findByEmail("john@example.com");
      
          expect(user).toBeTruthy();
          expect(user?.name).toBe("John Doe");
        });
      
        it("should return null if user not found", async () => {
          const user = await repository.findByEmail("nonexistent@example.com");
          expect(user).toBeNull();
        });
      });
      ```
      
      ## Frontend Testing with Testing Library
      
      ### Pattern 1: React Component Testing
      
      ```typescript
      // components/UserForm.tsx
      import { useState } from 'react';
      
      interface Props {
        onSubmit: (user: { name: string; email: string }) => void;
      }
      
      export function UserForm({ onSubmit }: Props) {
        const [name, setName] = useState('');
        const [email, setEmail] = useState('');
      
        const handleSubmit = (e: React.FormEvent) => {
          e.preventDefault();
          onSubmit({ name, email });
        };
      
        return (
          <form onSubmit={handleSubmit}>
            <input
              type="text"
              placeholder="Name"
              value={name}
              onChange={(e) => setName(e.target.value)}
              data-testid="name-input"
            />
            <input
              type="email"
              placeholder="Email"
              value={email}
              onChange={(e) => setEmail(e.target.value)}
              data-testid="email-input"
            />
            <button type="submit">Submit</button>
          </form>
        );
      }
      
      // components/UserForm.test.tsx
      import { render, screen, fireEvent } from '@testing-library/react';
      import { describe, it, expect, vi } from 'vitest';
      import { UserForm } from './UserForm';
      
      describe('UserForm', () => {
        it('should render form inputs', () => {
          render(<UserForm onSubmit={vi.fn()} />);
      
          expect(screen.getByPlaceholderText('Name')).toBeInTheDocument();
          expect(screen.getByPlaceholderText('Email')).toBeInTheDocument();
          expect(screen.getByRole('button', { name: 'Submit' })).toBeInTheDocument();
        });
      
        it('should update input values', () => {
          render(<UserForm onSubmit={vi.fn()} />);
      
          const nameInput = screen.getByTestId('name-input') as HTMLInputElement;
          const emailInput = screen.getByTestId('email-input') as HTMLInputElement;
      
          fireEvent.change(nameInput, { target: { value: 'John Doe' } });
          fireEvent.change(emailInput, { target: { value: 'john@example.com' } });
      
          expect(nameInput.value).toBe('John Doe');
          expect(emailInput.value).toBe('john@example.com');
        });
      
        it('should call onSubmit with form data', () => {
          const onSubmit = vi.fn();
          render(<UserForm onSubmit={onSubmit} />);
      
          fireEvent.change(screen.getByTestId('name-input'), {
            target: { value: 'John Doe' },
          });
          fireEvent.change(screen.getByTestId('email-input'), {
            target: { value: 'john@example.com' },
          });
          fireEvent.click(screen.getByRole('button', { name: 'Submit' }));
      
          expect(onSubmit).toHaveBeenCalledWith({
            name: 'John Doe',
            email: 'john@example.com',
          });
        });
      });
      ```
      
      ### Pattern 2: Testing Hooks
      
      ```typescript
      // hooks/useCounter.ts
      import { useState, useCallback } from "react";
      
      export function useCounter(initialValue = 0) {
        const [count, setCount] = useState(initialValue);
      
        const increment = useCallback(() => setCount((c) => c + 1), []);
        const decrement = useCallback(() => setCount((c) => c - 1), []);
        const reset = useCallback(() => setCount(initialValue), [initialValue]);
      
        return { count, increment, decrement, reset };
      }
      
      // hooks/useCounter.test.ts
      import { renderHook, act } from "@testing-library/react";
      import { describe, it, expect } from "vitest";
      import { useCounter } from "./useCounter";
      
      describe("useCounter", () => {
        it("should initialize with default value", () => {
          const { result } = renderHook(() => useCounter());
          expect(result.current.count).toBe(0);
        });
      
        it("should initialize with custom value", () => {
          const { result } = renderHook(() => useCounter(10));
          expect(result.current.count).toBe(10);
        });
      
        it("should increment count", () => {
          const { result } = renderHook(() => useCounter());
      
          act(() => {
            result.current.increment();
          });
      
          expect(result.current.count).toBe(1);
        });
      
        it("should decrement count", () => {
          const { result } = renderHook(() => useCounter(5));
      
          act(() => {
            result.current.decrement();
          });
      
          expect(result.current.count).toBe(4);
        });
      
        it("should reset to initial value", () => {
          const { result } = renderHook(() => useCounter(10));
      
          act(() => {
            result.current.increment();
            result.current.increment();
          });
      
          expect(result.current.count).toBe(12);
      
          act(() => {
            result.current.reset();
          });
      
          expect(result.current.count).toBe(10);
        });
      });
      ```
      
      ## Test Fixtures and Factories
      
      ```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,
        };
      }
      
      export function createUsersFixture(count: number): User[] {
        return Array.from({ length: count }, () => createUserFixture());
      }
      
      // Usage in tests
      import {
        createUserFixture,
        createUsersFixture,
      } from "../fixtures/user.fixture";
      
      describe("UserService", () => {
        it("should process user", () => {
          const user = createUserFixture({ name: "John Doe" });
          // Use user in test
        });
      
        it("should handle multiple users", () => {
          const users = createUsersFixture(10);
          // Use users in test
        });
      });
      ```
      
      ## Snapshot Testing
      
      ```typescript
      // components/UserCard.test.tsx
      import { render } from '@testing-library/react';
      import { describe, it, expect } from 'vitest';
      import { UserCard } from './UserCard';
      
      describe('UserCard', () => {
        it('should match snapshot', () => {
          const user = {
            id: '1',
            name: 'John Doe',
            email: 'john@example.com',
            avatar: 'https://example.com/avatar.jpg',
          };
      
          const { container } = render(<UserCard user={user} />);
      
          expect(container.firstChild).toMatchSnapshot();
        });
      
        it('should match snapshot with loading state', () => {
          const { container } = render(<UserCard loading />);
          expect(container.firstChild).toMatchSnapshot();
        });
      });
      ```
      
      ## Coverage Reports
      
      ```typescript
      // package.json
      {
        "scripts": {
          "test": "vitest",
          "test:coverage": "vitest --coverage",
          "test:ui": "vitest --ui"
        }
      }
      ```
      
      ## Common Patterns
      
      ### Test Organization
      
      ```typescript
      describe("UserService", () => {
        describe("createUser", () => {
          it("should create user successfully", () => {});
          it("should throw error if email exists", () => {});
          it("should hash password", () => {});
        });
      
        describe("updateUser", () => {
          it("should update user", () => {});
          it("should throw error if not found", () => {});
        });
      });
      ```
      
      ### Testing Promises
      
      ```typescript
      // Using async/await
      it("should fetch user", async () => {
        const user = await service.fetchUser("1");
        expect(user).toBeDefined();
      });
      
      // Testing rejections
      it("should throw error", async () => {
        await expect(service.fetchUser("invalid")).rejects.toThrow("Not found");
      });
      ```
      
      ### Testing Timers
      
      ```typescript
      import { vi } from "vitest";
      
      it("should call function after delay", () => {
        vi.useFakeTimers();
      
        const callback = vi.fn();
        setTimeout(callback, 1000);
      
        expect(callback).not.toHaveBeenCalled();
      
        vi.advanceTimersByTime(1000);
      
        expect(callback).toHaveBeenCalled();
      
        vi.useRealTimers();
      });
      ```
      
  • SKILL.md 14.3 KB
    ---
    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
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related