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
81%
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Passed
No known issues
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.
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/jestsrc/
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.spec.ts suffix.test/ directory.describe block per function/method under test. Nest describe for different scenarios.jest.fn() for simple stubs, jest.mock() for module-level mocking, jest.spyOn() for partial mocking.jest.clearAllMocks() in beforeEach or restoreMocks: true in config.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;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;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();
});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"),
})
);
});
});
});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"),
});
});
});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/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,
};
}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;
}
}
}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"`);
});
});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"),
});
});
});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);
});
});// 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();
});
});# 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@nestjs/testing Test.createTestingModule with Jest. Mock providers using .overrideProvider().useValue().supertest for HTTP-level integration tests: npm install -D supertest @types/supertest.github-actions-ci skill. Run npx jest --coverage --ci in the CI workflow. Upload coverage artifacts.vitest-testing-skill.# 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 speedUse 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.
181fcbc
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.