| name | unit-testing-patterns |
| description | Write effective unit tests — Arrange-Act-Assert structure, test boundary selection, mocking strategy, test doubles (spy/stub/mock/fake), parameterized tests, snapshot pitfalls, test naming, and Vitest/Jest configuration. Use when asked about "unit test", "write tests", "Jest", "Vitest", "mock function", "spy on", "stub", "test coverage", "describe/it/expect", "beforeEach/afterEach", "parameterized test", "test.each", "snapshot test", "how to test this function", or "unit testing best practices". Do NOT use for: E2E browser testing — see e2e-testing. Do NOT use for: contract testing — see contract-testing. Do NOT use for: load testing — see load-testing.
|
| origin | yamtam-original |
| license | MIT © 2026 Vũ Văn Tâm |
| version | 1.0.0 |
| compatibility | Vitest v2 (preferred), Jest v29. Patterns apply to both. |
When to Use
- Use when: writing a pure function that needs correctness verification
- Use when: a function has complex branching logic (happy + error paths)
- Use when: a module has side effects that need to be verified without running them
- Do NOT use for: React component integration — use RTL + Vitest for that
- Do NOT use for: full user journey testing — see e2e-testing
AAA Pattern
describe('calculateDiscount', () => {
it('applies 20% discount for premium users', () => {
const user = { tier: 'premium' };
const price = 100;
const result = calculateDiscount(user, price);
expect(result).toBe(80);
});
it('returns full price for standard users', () => {
const user = { tier: 'standard' };
expect(calculateDiscount(user, 100)).toBe(100);
});
});
Test Naming Convention
it('throws ValidationError when email is missing')
it('returns empty array when no results match')
it('calls sendEmail once when order is confirmed')
it('does NOT deduct balance when payment fails')
Mocking Strategy
import { vi, expect } from 'vitest';
const spy = vi.spyOn(emailService, 'send');
await processOrder(order);
expect(spy).toHaveBeenCalledOnce();
expect(spy).toHaveBeenCalledWith(expect.objectContaining({ to: order.userEmail }));
vi.mock('../lib/stripe', () => ({
createCharge: vi.fn().mockResolvedValue({ id: 'ch_123', status: 'succeeded' }),
}));
const fakeCache = new Map<string, string>();
const cacheService = {
get: (k: string) => Promise.resolve(fakeCache.get(k) ?? null),
set: (k: string, v: string) => Promise.(fakeCache.(k, v)),
};
Testing Async Code
describe('fetchUser', () => {
it('returns user data on success', async () => {
vi.mocked(api.get).mockResolvedValueOnce({ id: '1', name: 'Alice' });
const result = await fetchUser('1');
expect(result).toEqual({ id: '1', name: 'Alice' });
});
it('throws ApiError when user not found', async () => {
vi.mocked(api.get).mockRejectedValueOnce(new ApiError('NOT_FOUND', 404));
await expect(fetchUser('999')).rejects.toThrow(ApiError);
await expect(fetchUser('999')).rejects.toMatchObject({ code: 'NOT_FOUND' });
});
});
Parameterized Tests
describe('parseAmount', () => {
test.each([
['$1,000.00', 1000],
['$0.99', 0.99],
['$1,234,567.89', 1234567.89],
])('parses "%s" as %d', (input, expected) => {
expect(parseAmount(input)).toBe(expected);
});
test.each([
['', 'empty input'],
['abc', 'non-numeric'],
['-$100', 'negative'],
])('throws for "%s" (%s)', (input) => {
expect(() => parseAmount(input)).toThrow(ValidationError);
});
});
Snapshot Pitfalls
expect(render(<ProductCard product={product} />).container).toMatchSnapshot();
expect(renderIcon('star')).toMatchInlineSnapshot(`"<svg .../>"`);
expect(screen.getByRole('button', { name: 'Add to Cart' })).toBeInTheDocument();
expect(screen.getByText('$29.99')).toBeInTheDocument();
Vitest Config
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'node',
coverage: {
provider: 'v8',
reporter: ['text', 'html'],
thresholds: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
setupFiles: ['./test/setup.ts'],
},
});
What NOT to Test
✅ Test: pure functions, complex branching, error paths, side effects
❌ Skip: framework internals (React's own render logic)
❌ Skip: implementation details (internal variable names, private methods)
❌ Skip: trivial getters/setters with zero logic
❌ Skip: third-party library behavior — trust their tests
Anti-Fake-Pass Rules
Before claiming unit tests are done, you MUST show:
Reference: gates/anti-fake-pass-gate.md