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.
In this repository20 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- adopt-agentic-workflowSkill
- branch-and-commitSkill
- byeSkill
- commit-notationSkill
- commitSkill
- dedicated-browserSkill
- handling-pull-requestsSkill
- markdownSkill
- pnpmSkill
- reality-checkSkill
- schweizer-schreibweiseSkill
- show-your-workSkill
- test-driven-developmentSkill
- triage-ticketSkill
- typescript-strict-patternsSkill
- working-with-bitbucket-apiSkill
- working-with-bitbucket-webSkill
- working-with-jira-webSkill
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
- Very specific: line 215
All exports become jest.fn():
- Distinctive: line 230
Keep some original implementations:
- Distinctive: line 345
Every test follows Arrange-Act-Assert:
- Very specific: line 394
Use jest.mocked() for type safety:
≈ “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.
Files
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
| Name | Purpose | Example |
testObj | Object under test | const testObj = new OrderService() |
target | Function under test | const target = jest.fn() |
target* | Specific mock target | targetAdd, targetFetch |
mock* | Mock implementation | mockUserService, mockResponse |
actual | Result from code | const actual = testObj.calculate() |
expected | Expected value | const 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
| Scenario | Function | Rationale |
| Callback parameter | jest.fn() | Need to observe calls |
| Dependency injection | jest.fn() | Replace collaborator |
| Observe existing method | jest.spyOn() | Keep or mock implementation |
| External module | jest.mock() | Replace at import |
| Node built-in | jest.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 staysReset 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 goneRestore 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 restoredCleanup Summary
| Method | Clears History | Removes Implementation | Restores Original |
mockClear() | Yes | No | No |
mockReset() | Yes | Yes | No |
mockRestore() | Yes | Yes | Yes (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
| Function | Purpose |
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-Pattern | Problem | Fix |
| Testing mock behavior | Verifies mock, not code | Test real behavior |
| Conditional expects | Tests may silently skip | Remove conditions |
| Shared mutable state | Tests affect each other | Reset in beforeEach |
| Over-mocking | Tests prove nothing | Mock only boundaries |
| Magic timeout numbers | Fragile, slow | Use fake timers |
Bounded Scaffolding
Mock and fixture infrastructure is written for a test that exists, not for tests that might exist.
| Situation | Do |
| Test needs one stubbed function | jest.fn() inline in the test |
| Second test needs the same stub | Leave both inline; duplication is cheaper than a helper |
| Third test needs it | Extract a helper, no earlier |
Tempted to write __mocks__/ up front | Stop — 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
| Skill | Use For |
| test-driven-development | TDD workflow—Red-Green-Refactor, when to test |
| outer BDD loop (outside-in development) | Outer BDD loop for user stories |
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
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.

