Most codebases have tests that do not actually prevent bugs. They test implementation details that change when you refactor. They use mocks so heavily that they never test real behavior. They give false confidence — green tests but broken production. Good testing is about testing behavior, not implementation. This guide covers the patterns that actually catch bugs.
⚡ TL;DR: Test behavior, not implementation. Arrange-Act-Assert structure. Mock at the boundary (network, DB, time) not in the middle of your code. Test edge cases: empty, null, boundary values, error paths. Test names should describe what the system does, not which function is called.
What to test vs what not to test — unit testing best practices
// WRONG: testing implementation details
it('calls the formatDate helper', () => {
const spy = jest.spyOn(utils, 'formatDate');
renderComponent({ date: new Date() });
expect(spy).toHaveBeenCalled(); // This tests HOW not WHAT
// Passes even if the output is wrong!
// Breaks when you rename formatDate even if behavior is unchanged
});
// RIGHT: test what the user/caller actually sees
it('displays date in MM/DD/YYYY format', () => {
const { getByText } = render( );
expect(getByText('04/07/2026')).toBeInTheDocument();
// Tests BEHAVIOR — passes as long as output is correct
// Survives any internal refactoring
});
// WRONG: testing third-party library behavior
it('Array.map returns new array', () => { /* ... */ }); // They wrote that test!
// RIGHT: test YOUR code that uses the library
it('filters inactive users and returns sorted names', () => {
const users = [{ name: 'Bob', active: false }, { name: 'Alice', active: true }];
expect(getActiveUserNames(users)).toEqual(['Alice']);
});AAA pattern — Arrange, Act, Assert
// Every test has three clear sections:
describe('UserService.createUser', () => {
it('sends welcome email after successful account creation', async () => {
// ARRANGE: set up the world
const emailService = { sendWelcome: jest.fn().mockResolvedValue(true) };
const userRepo = { save: jest.fn().mockResolvedValue({ id: '123', name: 'Alice' }) };
const service = new UserService(userRepo, emailService);
// ACT: do the thing
const result = await service.createUser({ name: 'Alice', email: 'alice@example.com' });
// ASSERT: verify outcomes
expect(result.id).toBe('123');
expect(emailService.sendWelcome).toHaveBeenCalledWith(
expect.objectContaining({ email: 'alice@example.com' })
);
});
it('throws ValidationError when email is invalid', async () => {
const service = new UserService(mockRepo, mockEmail);
await expect(service.createUser({ name: 'Alice', email: 'not-an-email' }))
.rejects.toThrow(ValidationError);
});
});Mocking — mock at the boundary
// Mock at external boundaries: network, database, filesystem, time
// Do NOT mock internal functions or modules you wrote
// WRONG: mocking own code deeply
jest.mock('./utils/formatCurrency'); // Now testing with fake implementation
jest.mock('./services/calculateTax'); // Not testing real tax logic!
// RIGHT: mock external dependencies only
jest.mock('./db/client'); // Real boundary — DB calls
jest.mock('./email/smtp'); // Real boundary — email
jest.useFakeTimers(); // Real boundary — time
// Mock strategies:
// Spy: wrap real implementation, track calls
const spy = jest.spyOn(console, 'error').mockImplementation(() => {});
// Stub: replace with fake that returns specific value
const mockRepo = { findById: jest.fn().mockResolvedValue({ id: 1, name: 'Alice' }) };
// Fake: working implementation (in-memory DB for testing)
const repo = new InMemoryUserRepository(); // Behaves like real, fast, no I/OEdge cases — where bugs actually hide
describe('processOrder', () => {
// Happy path (everyone writes this)
it('processes valid order', async () => { /* ... */ });
// Edge cases (where bugs hide)
it('handles empty items array', async () => {
await expect(processOrder({ items: [] })).rejects.toThrow('Order must have items');
});
it('handles null customer', async () => {
await expect(processOrder({ customer: null, items: [item] })).rejects.toThrow();
});
it('handles item quantity of 0', async () => {
await expect(processOrder({ items: [{ qty: 0 }] })).rejects.toThrow();
});
it('handles negative price', async () => { /* ... */ });
it('handles order exactly at free shipping threshold', async () => { /* ... */ });
it('handles concurrent orders for same item (race condition)', async () => { /* ... */ });
it('rolls back if payment succeeds but inventory update fails', async () => { /* ... */ });
});Testing best practices cheat sheet
- ✅ Test behavior, not implementation — tests should survive refactoring
- ✅ One assertion per test concept — not one assert() per test
- ✅ Descriptive test names: “does X when Y given Z”
- ✅ Mock at external boundaries: network, DB, filesystem, time
- ✅ Test the error paths — that’s where bugs are
- ✅ Run tests in CI on every commit — fast feedback prevents bugs from shipping
- ❌ Don’t test private methods — they change. Test the public interface.
- ❌ Don’t aim for 100% coverage — aim for coverage of critical paths and edge cases
Unit testing discipline directly enables the CI/CD pipeline to work reliably — a green test suite that catches real bugs is what makes automated deployment safe. External reference: Jest best practices documentation.
Free Weekly Newsletter
🚀 Don’t Miss the Next Cheat Code
Related: AI Test Generation: Property Tests That Catch Race Conditions · AI PR Reviewers: Diff-Scoped Context Windows That Catch Real Bugs · Python typing Module: Generics and Protocols That Catch Real Bugs at Compile Time
Related: Embedding Drift Alerts: Catch Provider Vector Changes Before RAG Dies · Node.js Event Loop Lag: Catch p99 Stalls Before Users Feel Them · IAM Credential Canaries: Catch Leaked Laptop Keys Before Prod Blast Radius
Watch: 100 Days of AI on YouTube
Short videos from CheatCoders, one AI engineering topic per day.
Most viewed
- Python Decorators Explained: From Simple Wrappers to Production Patterns
- Claude Code vs Cursor vs GitHub Copilot: Choose by Workflow, and Set Up One Repo That Works With All Three
- AI Agent Frameworks in 2025: LangGraph vs CrewAI vs AutoGen vs Raw API
- Python String Methods: Every str Method With Real Production Examples
- REST API Design Best Practices: The Patterns That Make APIs a Joy to Use
Newly added
- AWS Systems Manager Parameter Store: Hierarchical Runtime Config for Coding Agents
- Amazon Cognito Identity Pools: Federate Coding-Agent Tool Callers Without Long-Lived Keys
- Amazon Athena for Coding Agents: Query Tool Audit Logs and Eval Traces on S3 Without a Warehouse
- AWS Batch for Coding Agents: Parallel Overnight PR Reviews and Long-Running Jobs Without Lambda Timeouts
- Amazon Bedrock Prompt Management: Version and A/B Test Coding-Agent System Prompts
Deep-dive PDF
Get the expanded guide for this post — extra diagrams-style checklists, failure modes, and a production walkthrough. Free when you subscribe to CheatCoders.
Already subscribed? or open the subscribe page.
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.
