| name | unit-testing |
| description | Unit testing patterns for frontend and backend. Trigger: When writing or reviewing unit tests for any layer. |
| license | Apache 2.0 |
| metadata | {"version":"1.1","type":"domain"} |
Unit Testing
Patterns for isolated, maintainable unit tests. Orchestrates jest and react-testing-library -- delegate to them for runner APIs and component queries.
When to Use
- Writing unit tests for frontend or backend code
- Reviewing test coverage, isolation, and structure
- Refactoring an existing test suite
Don't use for:
- Jest syntax and mock APIs -- delegate to jest skill
- React component queries -- delegate to react-testing-library skill
- Integration or E2E test strategy (different scope)
Critical Patterns
AAA Pattern (Arrange-Act-Assert)
Every test follows three phases separated by blank lines.
it('should apply discount', () => {
const order = createOrder({ subtotal: 200 });
const result = new PercentDiscount(10).apply(order);
expect(result.total).toBe(180);
});
it('discount', () => {
expect(new PercentDiscount(10).apply(createOrder({ subtotal: 200 })).total).toBe(180);
});
Test Isolation
let repo: jest.Mocked<UserRepo>;
let service: UserService;
beforeEach(() => {
repo = { findById: jest.fn(), save: jest.fn() } as any;
service = new UserService(repo);
});
const users: User[] = [];
it('adds', () => { users.push(newUser); });
it('checks', () => { expect(users).toHaveLength(1); });
One Assertion per Concept + Naming
Multiple expect calls are fine if they assert facets of the same outcome. Name tests as specs: should <expected> when <condition>.
it('should create user with defaults', () => {
const user = service.create({ name: 'Ada' });
expect(user.name).toBe('Ada');
expect(user.role).toBe('member');
});
it('create and delete', () => { });
Mock Boundaries (Mock I/O, Not Logic)
repo.findById.mockResolvedValue({ id: '1', balance: 50 });
const result = await service.withdraw('1', 30);
expect(result.balance).toBe(20);
jest.spyOn(service, 'withdraw').mockResolvedValue({ balance: 20 });
Positive and Negative Assertions
Every behavior has two sides: what SHOULD happen (positive) and what SHOULD NOT (negative). A test suite that only checks positive paths misses entire failure categories. Test both explicitly.
expect(user.role).toBe('member');
expect(repo.save).toHaveBeenCalledWith(expect.objectContaining({ name: 'Ada' }));
expect(() => service.create({ name: '' })).toThrow('Name required');
await expect(service.withdraw('1', 9999)).rejects.toThrow('Insufficient funds');
expect(result.errors).not.toContain('email');
expect(repo.save).not.toHaveBeenCalled();
Negative assertions use Jest matchers — see jest skill for .not, .toThrow(), .rejects.
Decision Tree
React component?
→ Delegate to react-testing-library skill
Pure function?
→ No mocks needed; call and assert
Service with dependencies?
→ Mock I/O via jest skill
Need setup per test?
→ beforeEach + afterEach
Multiple behaviors?
→ One it per behavior under describe
Error handling?
→ Dedicated it('should throw when ...') case
Unsure what to mock?
→ Mock anything touching network, disk, or clock
Example
import { AccountService } from './accountService';
import type { AccountRepo } from '../repositories/accountRepo';
describe('AccountService.withdraw', () => {
let repo: jest.Mocked<AccountRepo>;
let service: AccountService;
beforeEach(() => {
repo = { findById: jest.fn(), save: jest.fn() } as any;
service = new AccountService(repo);
});
it('should deduct and save', async () => {
repo.findById.mockResolvedValue({ id: '1', balance: 100 });
repo.save.mockResolvedValue({ id: '1', balance: 70 });
const account = await service.withdraw('1', 30);
expect(account.balance).toBe();
});
(, () => {
repo..({ : , : });
(service.(, ))..();
});
});
Edge Cases
- Flaky async: Always
await async operations; use fake timers for time-dependent logic
- Coverage gaps: Write explicit tests for
else, catch, and default branches
- Test coupling: If renaming private method breaks tests, test public API only
- Shared utilities: Extract factories (
createUser()) into test/helpers/
- Non-deterministic data: Seed random values or freeze
Date.now()
Checklist
Resources