Writing high-quality unit tests for JavaScript and TypeScript using Jest. Covers test structure (AAA pattern, USE naming), breaking dependencies (stubs, mocks, dependency injection), testing async code (promises, callbacks, timers), avoiding flaky tests, and test-driven development. Use when writing tests, debugging test failures, refactoring tests for maintainability, or questions about Jest, TDD, mocks, stubs, or test best practices.
Provides expert guidance for writing maintainable JavaScript/TypeScript unit tests with Jest. Uses AAA pattern, dependency injection, and proper mocking strategies. Trigger when writing tests, debugging failures, refactoring, or asking about Jest, TDD, mocks, or test best practices.
/plugin marketplace add el-feo/ai-context/plugin install js-ts@jebs-dev-toolsThis skill inherits all available tools. When active, it can use any tool Claude has access to.
EXAMPLES.mdREFERENCE.mdExpert guidance for writing maintainable, trustworthy unit tests in JavaScript and TypeScript using Jest. Based on "The Art of Unit Testing, Third Edition" by Roy Osherove with Vladimir Khorikov (Manning, 2024).
When helping users with unit testing:
A unit of work is all actions between an entry point (function/method we trigger) and one or more exit points (observable results).
Three types of exit points:
Must have:
Avoid:
test('sum with two numbers returns their sum', () => {
// Arrange - set up test data
const input = '1,2';
// Act - call the unit of work
const result = sum(input);
// Assert - verify the outcome
expect(result).toBe(3);
});
Format: **[U]**nit, **[S]**cenario, **[E]**xpectation
// Good examples
test('sum, with two valid numbers, returns their sum', () => { ... });
test('verify, with no uppercase letter, returns false', () => { ... });
test('save, during maintenance window, throws exception', () => { ... });
describe('Password Verifier', () => {
describe('one uppercase rule', () => {
test('given no uppercase, returns false', () => {
const verifier = makeVerifier([oneUpperCaseRule]);
expect(verifier.verify('abc')).toBe(false);
});
test('given one uppercase, returns true', () => {
const verifier = makeVerifier([oneUpperCaseRule]);
expect(verifier.verify('Abc')).toBe(true);
});
});
});
Use factory methods instead of beforeEach() to avoid scroll fatigue and keep tests self-contained.
test('processes input and returns result', () => {
const result = calculateTotal([10, 20, 30]);
expect(result).toBe(60);
});
test('adds item to cart and updates count', () => {
const cart = new ShoppingCart();
cart.addItem('apple');
expect(cart.itemCount()).toBe(1);
expect(cart.contains('apple')).toBe(true);
});
Only for testing third-party calls (exit points):
test('save calls logger with correct message', () => {
const mockLogger = { info: jest.fn() };
const repository = new Repository(mockLogger);
repository.save({ id: 1, name: 'test' });
expect(mockLogger.info).toHaveBeenCalledWith('Saved item 1');
});
Important: Use mocks only for exit points. Have one mock per test maximum. Most tests (95%+) should be return-value or state-based.
Break dependencies when code relies on:
1. Parameter Injection (Simplest)
// Before - time dependency baked in
const verify = (input) => {
const day = moment().day(); // Hard to test!
if (day === 0 || day === 6) throw Error("Weekend!");
};
// After - time injected
const verify = (input, currentDay) => {
if (currentDay === 0 || currentDay === 6) throw Error("Weekend!");
};
// Test with full control
test('on weekends, throws exception', () => {
expect(() => verify('input', 0)).toThrow("Weekend!");
});
2. Functional Injection
const verify = (logger) => (input) => {
logger.info('Verifying');
return input.length > 5;
};
// Test
test('verify logs attempt', () => {
const stubLogger = { info: jest.fn() };
const verifyFn = verify(stubLogger);
verifyFn('password');
});
3. Constructor Injection (OOP)
class Verifier {
constructor(private logger: ILogger) {}
verify(input: string): boolean {
this.logger.info('Verifying');
return true;
}
}
// Test
test('verify calls logger', () => {
const mockLogger = { info: jest.fn() };
const verifier = new Verifier(mockLogger);
verifier.verify('input');
expect(mockLogger.info).toHaveBeenCalled();
});
Stubs (incoming dependencies):
Mocks (outgoing dependencies):
// Stub - provides data IN
const stubDatabase = {
getUser: () => ({ id: 1, name: 'John' })
};
// Mock - verifies calls OUT
const mockLogger = {
info: jest.fn()
};
test('getUserName retrieves name from database and logs', () => {
const service = new UserService(stubDatabase, mockLogger);
const name = service.getUserName(1);
expect(name).toBe('John'); // Return value assertion
expect(mockLogger.info).toHaveBeenCalledWith('Retrieved user 1'); // Mock assertion
});
Extract pure logic from async operations:
// Before - everything mixed
const isWebsiteAlive = async () => {
const resp = await fetch('http://example.com');
if (!resp.ok) throw resp.statusText;
const text = await resp.text();
return text.includes('illustrative')
? { success: true }
: { success: false, status: 'missing text' };
};
// After - extract testable logic
const processFetchContent = (text) => {
return text.includes('illustrative')
? { success: true }
: { success: false, status: 'missing text' };
};
// Fast, synchronous unit test
test('with good content, returns success', () => {
const result = processFetchContent('illustrative');
expect(result.success).toBe(true);
});
Wrap async dependencies behind testable interfaces:
// network-adapter.js - wrapper for fetch
const fetchUrlText = async (url) => {
const resp = await fetch(url);
return resp.ok
? { ok: true, text: await resp.text() }
: { ok: false, text: resp.statusText };
};
// website-verifier.js - inject adapter
const isWebsiteAlive = async (network) => {
const result = await network.fetchUrlText('http://example.com');
if (!result.ok) throw result.text;
return result.text.includes('illustrative');
};
// Test with fake adapter (synchronous!)
test('with good content, returns true', async () => {
const fakeNetwork = {
fetchUrlText: () => ({ ok: true, text: 'illustrative' })
};
const result = await isWebsiteAlive(fakeNetwork);
expect(result).toBe(true);
});
test('calls callback after delay', () => {
jest.useFakeTimers();
const callback = jest.fn();
delayedGreeting(callback);
expect(callback).not.toHaveBeenCalled();
jest.advanceTimersByTime(1000);
expect(callback).toHaveBeenCalledWith('hello');
jest.useRealTimers();
});
Can you answer YES to all these?
If NO to any → Review the corresponding section in REFERENCE.md
For comprehensive code examples covering all patterns and scenarios, see EXAMPLES.md.
Test Structure:
Dependencies:
Async Code:
Maintainability:
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.