agentleFS
quatico-solutionsquatico-solutions/agent-skills/skills/jest-testing-conventions/SKILL.md

jest-testing-conventions

Skill2 starsChanged 4 months agoMITView on GitHub

Use when writing unit tests with Jest. Covers naming conventions (testObj, target, mock, actual, expected), jest.fn/spyOn/mock mastery, clear/reset/restore differences, module mocking, fake timers, and AAA pattern. Triggers: Jest, unit test, mock, spy, stub, jest.fn, jest.spyOn, jest.mock.

The file

We split this skill into the rules it gives an agent

4 rules. About 0 are common, 2 are distinctive, and 2 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈2

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 2

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    All exports become jest.fn():

    line 215
  2. Distinctive:

    Keep some original implementations:

    line 230
  3. Distinctive:

    Every test follows Arrange-Act-Assert:

    line 345
  4. Very specific:

    Use jest.mocked() for type safety:

    line 394

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Jest Testing Conventions

TDD Flow

Red-Green-Refactor, including the stopping rule and verification budget, lives in the test-driven-development skill. This skill covers the Jest mechanics only.

Overview

Master three functions: jest.fn(), jest.spyOn(), jest.mock().

Core principle: Mocks are necessary evil. Prefer real code. Use mocks only when unavoidable.

Naming Conventions

Variable Naming

NamePurposeExample
testObjObject under testconst testObj = new OrderService()
targetFunction under testconst target = jest.fn()
target*Specific mock targettargetAdd, targetFetch
mock*Mock implementationmockUserService, mockResponse
actualResult from codeconst actual = testObj.calculate()
expectedExpected valueconst expected = 42

Test Structure

describe("OrderService", () => {
    describe("calculateTotal", () => {
        it("returns sum of item prices", () => {
            // Arrange
            const testObj = new OrderService();
            const mockItems = [{ price: 10 }, { price: 20 }];
            const expected = 30;

            // Act
            const actual = testObj.calculateTotal(mockItems);

            // Assert
            expect(actual).toBe(expected);
        });
    });
});

The Three Core Functions

jest.fn() - Stub a Function

Creates a mock function that can be observed and configured.

// Basic stub
const target = jest.fn();
target("foo");
expect(target).toHaveBeenCalledWith("foo");

// With return value
const target = jest.fn().mockReturnValue("bar");
expect(target()).toBe("bar");

// With implementation
const target = jest.fn((x) => x * 2);
expect(target(5)).toBe(10);

// With promise resolution
const target = jest.fn().mockResolvedValue({ data: "result" });
await expect(target()).resolves.toEqual({ data: "result" });

// One-time implementation
const target = jest.fn()
    .mockReturnValueOnce("first")
    .mockReturnValueOnce("second")
    .mockReturnValue("default");

jest.spyOn() - Spy on Existing Function

Observes calls while optionally mocking implementation.

import * as math from "./math";

// Spy without changing implementation
const target = jest.spyOn(math, "add");
expect(math.add(1, 2)).toBe(3);  // Original runs
expect(target).toHaveBeenCalledWith(1, 2);

// Spy with mock implementation
jest.spyOn(math, "add").mockReturnValue(999);
expect(math.add(1, 2)).toBe(999);  // Mock runs

// Spy on object method
const obj = { getValue: () => 42 };
jest.spyOn(obj, "getValue").mockReturnValue(100);
expect(obj.getValue()).toBe(100);

// Spy on getter
jest.spyOn(obj, "value", "get").mockReturnValue(100);

jest.mock() - Mock a Module

Replaces entire module exports with mocks.

import { fetchUser } from "./userService";
import * as api from "./api";

jest.mock("./api");

test("fetchUser calls api.get", async () => {
    (api.get as jest.Mock).mockResolvedValue({ name: "John" });

    const result = await fetchUser(123);

    expect(api.get).toHaveBeenCalledWith("/users/123");
    expect(result.name).toBe("John");
});

When to Use Each

ScenarioFunctionRationale
Callback parameterjest.fn()Need to observe calls
Dependency injectionjest.fn()Replace collaborator
Observe existing methodjest.spyOn()Keep or mock implementation
External modulejest.mock()Replace at import
Node built-injest.mock()e.g., fs, path

Clear vs Reset vs Restore

Three different cleanup operations with distinct behaviors.

Clear Mocks

Clears call history. Implementation stays.

const target = jest.fn().mockReturnValue(42);
target();
target();

jest.clearAllMocks();  // or target.mockClear()

expect(target).not.toHaveBeenCalled();  // History cleared
expect(target()).toBe(42);  // Implementation stays

Reset Mocks

Clears history AND removes implementation.

const target = jest.fn().mockReturnValue(42);
target();

jest.resetAllMocks();  // or target.mockReset()

expect(target).not.toHaveBeenCalled();  // History cleared
expect(target()).toBeUndefined();  // Implementation gone

Restore Mocks

Restores original implementation (only for spies).

const obj = { getValue: () => 42 };
jest.spyOn(obj, "getValue").mockReturnValue(100);

expect(obj.getValue()).toBe(100);  // Mock

jest.restoreAllMocks();  // or spy.mockRestore()

expect(obj.getValue()).toBe(42);  // Original restored

Cleanup Summary

MethodClears HistoryRemoves ImplementationRestores Original
mockClear()YesNoNo
mockReset()YesYesNo
mockRestore()YesYesYes (spies only)

Recommended Config

// jest.config.ts
export default {
    clearMocks: true,    // Clear history between tests
    resetMocks: true,    // Also reset implementations
    restoreMocks: true,  // Also restore spies
};

Module Mocking Patterns

Full Mock

All exports become jest.fn():

jest.mock("./math");

import { add, subtract } from "./math";

test("mocked module", () => {
    (add as jest.Mock).mockReturnValue(10);
    expect(add(1, 2)).toBe(10);
});

Partial Mock

Keep some original implementations:

jest.mock("./utils", () => {
    const original = jest.requireActual("./utils");
    return {
        ...original,
        formatDate: jest.fn().mockReturnValue("2024-01-01"),
    };
});

Manual Mock

Create __mocks__/moduleName.ts:

src/
├── services/
│   ├── userService.ts
│   └── __mocks__/
│       └── userService.ts
// __mocks__/userService.ts
export const getUser = jest.fn().mockResolvedValue({ name: "Mock User" });

Mocking Classes

Mock Constructor

jest.mock("./SoundPlayer", () => {
    return jest.fn().mockImplementation(() => ({
        play: jest.fn(),
        stop: jest.fn(),
    }));
});

Mock Static Method

jest.spyOn(SoundPlayer, "getSupportedFormats")
    .mockReturnValue(["mp3", "wav"]);

Mock Instance Method

const mockPlay = jest.fn();
SoundPlayer.mockImplementation(() => ({
    play: mockPlay,
}));

// Later
expect(mockPlay).toHaveBeenCalled();

Fake Timers

Control time-based code without waiting.

Basic Usage

beforeAll(() => {
    jest.useFakeTimers();
});

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

test("debounce waits 300ms", () => {
    const callback = jest.fn();
    debounce(callback, 300)();

    expect(callback).not.toHaveBeenCalled();

    jest.advanceTimersByTime(300);

    expect(callback).toHaveBeenCalled();
});

Timer Functions

FunctionPurpose
jest.useFakeTimers()Enable fake timers
jest.useRealTimers()Restore real timers
jest.advanceTimersByTime(ms)Move time forward
jest.runAllTimers()Execute all pending timers
jest.runOnlyPendingTimers()Execute current pending only
jest.clearAllTimers()Remove pending timers

With Promises

test("async with timers", async () => {
    const promise = asyncDelay(1000);

    jest.advanceTimersByTime(1000);

    await promise;  // Now resolves
});

Best Practices

AAA Pattern

Every test follows Arrange-Act-Assert:

test("calculates discount for premium users", () => {
    // Arrange
    const testObj = new PricingService();
    const mockUser = { isPremium: true };
    const basePrice = 100;

    // Act
    const actual = testObj.calculatePrice(basePrice, mockUser);

    // Assert
    expect(actual).toBe(90);  // 10% discount
});

One Assertion Focus

Each test verifies one behavior:

// Bad - multiple behaviors
test("order processing", () => {
    expect(order.validate()).toBe(true);
    expect(order.calculateTotal()).toBe(100);
    expect(order.save()).resolves.toBeDefined();
});

// Good - separate tests
test("validates order", () => { ... });
test("calculates total", () => { ... });
test("saves order", () => { ... });

Mock Console

Suppress expected console output:

// beforeEach: the recommended config's restoreMocks undoes spies before every test
beforeEach(() => {
    jest.spyOn(console, "warn").mockImplementation(() => {});
    jest.spyOn(console, "error").mockImplementation(() => {});
});

Typed Mocks

Use jest.mocked() for type safety:

import { getUser } from "./userService";

jest.mock("./userService");

const mockGetUser = jest.mocked(getUser);

test("typed mock", async () => {
    mockGetUser.mockResolvedValue({ id: 1, name: "John" });
    // TypeScript knows the return type
});

Common Matchers

// Called
expect(mock).toHaveBeenCalled();
expect(mock).toHaveBeenCalledTimes(3);
expect(mock).toHaveBeenCalledWith(arg1, arg2);
expect(mock).toHaveBeenLastCalledWith(arg1);
expect(mock).toHaveBeenNthCalledWith(2, arg1);

// Return values
expect(mock).toHaveReturned();
expect(mock).toHaveReturnedWith(value);
expect(mock).toHaveLastReturnedWith(value);

// Flexible matchers
expect(mock).toHaveBeenCalledWith(
    expect.any(Number),
    expect.objectContaining({ id: 1 }),
    expect.arrayContaining([1, 2]),
    expect.stringMatching(/pattern/),
);

Anti-Patterns to Avoid

Anti-PatternProblemFix
Testing mock behaviorVerifies mock, not codeTest real behavior
Conditional expectsTests may silently skipRemove conditions
Shared mutable stateTests affect each otherReset in beforeEach
Over-mockingTests prove nothingMock only boundaries
Magic timeout numbersFragile, slowUse fake timers

Bounded Scaffolding

Mock and fixture infrastructure is written for a test that exists, not for tests that might exist.

SituationDo
Test needs one stubbed functionjest.fn() inline in the test
Second test needs the same stubLeave both inline; duplication is cheaper than a helper
Third test needs itExtract a helper, no earlier
Tempted to write __mocks__/ up frontStop — manual mocks are for modules already mocked in two or more test files

Do not build a test utility module, a factory, a custom matcher or a shared setup harness until at least three tests need it. Two duplicated arrangements are fine and often clearer than an abstraction.

Do not add coverage thresholds, custom reporters or CI test-gating as part of writing a test. That is separate work with a separate owner.

Verification Checklist

Before committing tests:

  • AAA pattern (Arrange-Act-Assert)
  • One behavior per test
  • Clear variable names (testObj, target, actual, expected)
  • Mocks cleaned up (config or manual)
  • No conditional assertions
  • Fake timers for time-based code
  • Console mocked if expected output

Boxes checked? Stop. Adding checks of your own to this list is the failure mode this list guards against.

Related Skills

SkillUse For
test-driven-developmentTDD workflow—Red-Green-Refactor, when to test
outer BDD loop (outside-in development)Outer BDD loop for user stories

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.