No. 005 · testing
Jest Testing Strategies
Tests that catch regressions without becoming a maintenance burden
Most test suites fail because they test implementation details instead of behavior. When tests break on every refactor, developers stop trusting them and stop writing them.
What to test
Test behavior, not implementation:
// Bad — tests implementation detail
expect(component.state.isLoading).toBe(true);
// Good — tests observable behavior
expect(screen.getByText('Loading...')).toBeInTheDocument();
The test pyramind:
- Many unit tests: fast, focused, test one function or component in isolation
- Fewer integration tests: verify that modules work together correctly
- Minimal E2E tests: cover critical user journeys only
Test the contract, not the mechanism:
- If a function returns a sorted array, test that the output is sorted
- Don’t test that it calls
.sort()internally — that’s an implementation detail that will change
File structure
Co-locate test files with source files:
src/
├── services/
│ ├── auth.ts
│ ├── auth.test.ts ← next to the file it tests
│ └── auth.integration.ts ← for integration tests
└── utils/
├── format.ts
└── format.test.ts
This makes it obvious what’s tested and what isn’t, and keeps related files together when you’re working on a feature.
Mocking strategy
Mock at the boundary, not inside the unit:
// Good — mock the external API, not the service's internals
jest.mock('../services/api', () => ({
fetchUser: jest.fn(),
}));
// Bad — mock the service's internal method
jest.spyOn(userService, 'internalMethod');
Use jest.fn() for callbacks, manual mocks for modules:
// For individual functions
const mockFetch = jest.fn().mockResolvedValue({ data: [] });
// For entire modules
jest.mock('../db', () => ({
query: jest.fn(),
connect: jest.fn(),
}));
When to use jest.spyOn: Only when you need to observe a call
without replacing the implementation (e.g., spying on
console.error).
Async testing
Always await your assertions:
it('loads user data', async () => {
render(<UserProfile userId="123" />);
expect(await screen.findByText('Jane Doe')).toBeInTheDocument();
});
Use userEvent over fireEvent — it better simulates real user
interactions with proper event sequencing.
When not to test
- Trivial getters/setters with no logic
- Third-party library internals
- Exact CSS values (use visual regression tools instead)
- Code that’s pure configuration with no behavior
See references/mock-patterns.md and references/coverage-guide.md
for more detail.
When it triggers
- writing unit tests
- mocking a dependency in tests
- tests are brittle and break on refactor
- deciding what to test