CtrlK
BlogDocsLog inGet started
Tessl Logo

jest-testing-skill

Set up Jest 30+ with TypeScript for unit tests, integration tests, mocking (jest.fn, jest.mock, jest.spyOn), coverage configuration, custom matchers, snapshot testing, and setup/teardown patterns.

84

Quality

81%

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Passed

No known issues

SKILL.md
Quality
Evals
Security

Jest Testing Skill

Set up Jest 30+ with TypeScript for unit tests, integration tests, mocking (jest.fn, jest.mock, jest.spyOn), coverage configuration, custom matchers, snapshot testing, and setup/teardown patterns.

Prerequisites

  • Node.js >= 20.x
  • TypeScript >= 5.3
  • npm or pnpm

Scaffold Command

npm install -D jest @jest/globals ts-jest @types/jest

# Initialize Jest config
npx ts-jest config:init

# For ESM projects, use jest with SWC for faster transforms
npm install -D @swc/core @swc/jest

Project Structure

src/
  modules/
    users/
      users.service.ts
      users.service.spec.ts       # Unit test — co-located with source
      users.repository.ts
      __mocks__/                   # Manual mocks for this module
        users.repository.ts
test/
  setup.ts                        # Global test setup
  helpers/
    factories.ts                  # Test data factories
    matchers.ts                   # Custom matchers
  integration/
    users.integration.spec.ts     # Integration tests
jest.config.ts                    # Jest configuration

Key Conventions

  • Co-locate unit tests with source files using .spec.ts suffix.
  • Place integration/e2e tests in a separate test/ directory.
  • One describe block per function/method under test. Nest describe for different scenarios.
  • Name test cases with "should [expected behavior] when [condition]" format.
  • Use jest.fn() for simple stubs, jest.mock() for module-level mocking, jest.spyOn() for partial mocking.
  • Always clear mocks between tests: jest.clearAllMocks() in beforeEach or restoreMocks: true in config.
  • Use test data factories instead of inline object literals to reduce duplication.
  • Keep coverage thresholds meaningful (aim for 80%+ on branches, not 100% for vanity).

Essential Patterns

Jest Configuration (jest.config.ts)

import type { Config } from "jest";

const config: Config = {
  preset: "ts-jest",
  testEnvironment: "node",
  roots: ["<rootDir>/src", "<rootDir>/test"],
  testMatch: ["**/*.spec.ts", "**/*.test.ts"],
  moduleNameMapper: {
    "^@/(.*)$": "<rootDir>/src/$1",
  },
  setupFilesAfterSetup: ["<rootDir>/test/setup.ts"],
  collectCoverageFrom: [
    "src/**/*.ts",
    "!src/**/*.spec.ts",
    "!src/**/*.d.ts",
    "!src/**/index.ts",
  ],
  coverageThresholds: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80,
    },
  },
  clearMocks: true,
  restoreMocks: true,
};

export default config;

Jest Config with SWC (Faster)

import type { Config } from "jest";

const config: Config = {
  testEnvironment: "node",
  roots: ["<rootDir>/src", "<rootDir>/test"],
  testMatch: ["**/*.spec.ts", "**/*.test.ts"],
  transform: {
    "^.+\\.ts$": ["@swc/jest"],
  },
  moduleNameMapper: {
    "^@/(.*)$": "<rootDir>/src/$1",
  },
  clearMocks: true,
  restoreMocks: true,
};

export default config;

Global Setup (test/setup.ts)

// Extend expect with custom matchers (optional)
import "./helpers/matchers";

// Set test timeout
jest.setTimeout(10000);

// Suppress console.log in tests (optional)
beforeAll(() => {
  jest.spyOn(console, "log").mockImplementation(() => {});
});

afterAll(() => {
  jest.restoreAllMocks();
});

Unit Test with Mocking (users.service.spec.ts)

import { describe, it, expect, jest, beforeEach } from "@jest/globals";
import { UsersService } from "./users.service";
import { UsersRepository } from "./users.repository";

// Mock the repository module
jest.mock("./users.repository");

describe("UsersService", () => {
  let service: UsersService;
  let mockRepo: jest.Mocked<UsersRepository>;

  beforeEach(() => {
    mockRepo = new UsersRepository() as jest.Mocked<UsersRepository>;
    service = new UsersService(mockRepo);
  });

  describe("findById", () => {
    it("should return the user when found", async () => {
      const mockUser = { id: "1", email: "test@example.com", name: "Test User" };
      mockRepo.findById.mockResolvedValue(mockUser);

      const result = await service.findById("1");

      expect(result).toEqual(mockUser);
      expect(mockRepo.findById).toHaveBeenCalledWith("1");
      expect(mockRepo.findById).toHaveBeenCalledTimes(1);
    });

    it("should throw NotFoundException when user not found", async () => {
      mockRepo.findById.mockResolvedValue(null);

      await expect(service.findById("999")).rejects.toThrow("User not found");
    });
  });

  describe("create", () => {
    it("should hash the password before saving", async () => {
      const input = { email: "new@example.com", name: "New User", password: "plain123" };
      const saved = { id: "2", ...input, password: "hashed" };
      mockRepo.create.mockResolvedValue(saved);

      const result = await service.create(input);

      expect(result.id).toBe("2");
      // Verify password was not stored as plain text
      expect(mockRepo.create).toHaveBeenCalledWith(
        expect.objectContaining({
          email: "new@example.com",
          password: expect.not.stringContaining("plain123"),
        })
      );
    });
  });
});

jest.spyOn Example

import { describe, it, expect, jest } from "@jest/globals";
import * as emailService from "../services/email.service";
import { notifyUser } from "./notification.service";

describe("notifyUser", () => {
  it("should send an email with the correct subject", async () => {
    const sendSpy = jest
      .spyOn(emailService, "sendEmail")
      .mockResolvedValue({ messageId: "abc" });

    await notifyUser("user@example.com", "Welcome!");

    expect(sendSpy).toHaveBeenCalledWith({
      to: "user@example.com",
      subject: "Welcome!",
      body: expect.stringContaining("Welcome"),
    });
  });
});

jest.fn() — Standalone Mock Functions

describe("processItems", () => {
  it("should call the callback for each item", () => {
    const callback = jest.fn<(item: string) => void>();
    const items = ["a", "b", "c"];

    processItems(items, callback);

    expect(callback).toHaveBeenCalledTimes(3);
    expect(callback).toHaveBeenNthCalledWith(1, "a");
    expect(callback).toHaveBeenNthCalledWith(2, "b");
    expect(callback).toHaveBeenNthCalledWith(3, "c");
  });
});

Test Data Factory (test/helpers/factories.ts)

interface User {
  id: string;
  email: string;
  name: string;
  role: string;
  createdAt: Date;
}

let counter = 0;

export function buildUser(overrides: Partial<User> = {}): User {
  counter++;
  return {
    id: `user-${counter}`,
    email: `user${counter}@example.com`,
    name: `Test User ${counter}`,
    role: "user",
    createdAt: new Date("2024-01-01"),
    ...overrides,
  };
}

Custom Matchers (test/helpers/matchers.ts)

expect.extend({
  toBeWithinRange(received: number, floor: number, ceiling: number) {
    const pass = received >= floor && received <= ceiling;
    return {
      pass,
      message: () =>
        `expected ${received} ${pass ? "not " : ""}to be within range ${floor} - ${ceiling}`,
    };
  },
});

declare global {
  namespace jest {
    interface Matchers<R> {
      toBeWithinRange(floor: number, ceiling: number): R;
    }
  }
}

Snapshot Testing

import { describe, it, expect } from "@jest/globals";
import { formatUserResponse } from "./formatters";

describe("formatUserResponse", () => {
  it("should match the expected output shape", () => {
    const user = {
      id: "1",
      email: "test@example.com",
      name: "Test User",
      createdAt: new Date("2024-01-01T00:00:00Z"),
    };

    expect(formatUserResponse(user)).toMatchSnapshot();
  });

  // Inline snapshot — no separate file
  it("should format the date correctly", () => {
    const result = formatDate(new Date("2024-06-15T12:00:00Z"));
    expect(result).toMatchInlineSnapshot(`"June 15, 2024"`);
  });
});

Async Error Testing

describe("async operations", () => {
  it("should reject with a specific error", async () => {
    await expect(fetchData("invalid")).rejects.toThrow("Not found");
  });

  it("should reject with an error matching properties", async () => {
    await expect(fetchData("invalid")).rejects.toMatchObject({
      statusCode: 404,
      message: expect.stringContaining("not found"),
    });
  });
});

Timer Mocking

describe("debounce", () => {
  beforeEach(() => {
    jest.useFakeTimers();
  });

  afterEach(() => {
    jest.useRealTimers();
  });

  it("should call the function after the delay", () => {
    const fn = jest.fn();
    const debounced = debounce(fn, 300);

    debounced();
    expect(fn).not.toHaveBeenCalled();

    jest.advanceTimersByTime(300);
    expect(fn).toHaveBeenCalledTimes(1);
  });
});

Mocking HTTP APIs (MSW)

// tests/mocks/handlers.ts
import { http, HttpResponse } from "msw";

export const handlers = [
  http.get("/api/users", () => {
    return HttpResponse.json([
      { id: "1", name: "Alice", email: "alice@test.com" },
    ]);
  }),
  http.post("/api/users", async ({ request }) => {
    const body = await request.json();
    return HttpResponse.json({ id: "2", ...body }, { status: 201 });
  }),
];
// tests/mocks/server.ts
import { setupServer } from "msw/node";
import { handlers } from "./handlers";

export const server = setupServer(...handlers);
// tests/setup.ts — add MSW lifecycle hooks
import { server } from "./mocks/server";

beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
// tests/integration/users.integration.spec.ts
import { describe, it, expect } from "@jest/globals";
import { http, HttpResponse } from "msw";
import { server } from "../mocks/server";
import { fetchUsers } from "../../src/modules/users/users.service";

describe("Users API integration", () => {
  it("should fetch users from the API", async () => {
    const users = await fetchUsers();

    expect(users).toHaveLength(1);
    expect(users[0]).toMatchObject({ id: "1", name: "Alice" });
  });

  it("should handle API errors", async () => {
    server.use(
      http.get("/api/users", () => {
        return HttpResponse.json({ message: "Server error" }, { status: 500 });
      })
    );

    await expect(fetchUsers()).rejects.toThrow();
  });
});

Common Commands

# Run all tests
npx jest

# Run tests in watch mode
npx jest --watch

# Run a specific test file
npx jest src/modules/users/users.service.spec.ts

# Run tests matching a pattern
npx jest --testNamePattern="should return the user"

# Run with coverage
npx jest --coverage

# Update snapshots
npx jest --updateSnapshot

# Run only changed files (CI-friendly)
npx jest --changedSince=main

# Verbose output
npx jest --verbose

# Debug a test
node --inspect-brk node_modules/.bin/jest --runInBand src/modules/users/users.service.spec.ts

Integration Notes

  • NestJS: Use @nestjs/testing Test.createTestingModule with Jest. Mock providers using .overrideProvider().useValue().
  • Express/Fastify: Use supertest for HTTP-level integration tests: npm install -D supertest @types/supertest.
  • Database: For integration tests, use a test database. Run migrations before the test suite, truncate between tests.
  • CI/CD: Pair with github-actions-ci skill. Run npx jest --coverage --ci in the CI workflow. Upload coverage artifacts.
  • Vitest: For Vite-based projects, prefer Vitest over Jest (faster, native ESM). See vitest-testing-skill.

Test Database Setup

# docker-compose.test.yml
services:
  test-db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: testdb
      POSTGRES_USER: test
      POSTGRES_PASSWORD: test
    ports:
      - "5433:5432"
    tmpfs:
      - /var/lib/postgresql/data  # RAM-backed for speed

Use DATABASE_URL=postgresql://test:test@localhost:5433/testdb in your test environment. Start with docker compose -f docker-compose.test.yml up -d before running integration tests.

Repository
achreftlili/deep-dev-skills
Last updated
Created

Is this your skill?

If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.