A Pragmatic Testing Strategy for Real Projects
Most testing advice is either too academic or too dogmatic. The testing pyramid tells you to write lots of unit tests, fewer integration tests, and even fewer end-to-end tests.
In practice? Integration tests give you the most confidence per line of test code.
The Testing Trophy
Kent C. Dodds proposed the “testing trophy” as a better model:
- Static analysis (TypeScript, ESLint) - catches typos and obvious errors
- Unit tests - for pure logic and algorithms
- Integration tests - test features as users experience them
- E2E tests - critical user paths only
The trophy shape puts integration tests at the widest point. That’s where the most value is.
What to Unit Test
Unit test things that are:
- Pure functions with complex logic
- Algorithms that have edge cases
- Utility functions used across the codebase
// Worth unit testing - complex logic, many edge cases
function parseTimeRange(input: string): { start: Date; end: Date } {
// ...
}
// Not worth unit testing - just plumbing
function getUserById(id: string) {
return db.users.findUnique({ where: { id } });
}
Don’t unit test wrappers, simple CRUD operations, or framework code.
What to Integration Test
Integration tests should exercise real features through their public interface:
describe('User registration', () => {
it('creates a user and sends a welcome email', async () => {
const response = await request(app)
.post('/api/register')
.send({ email: 'test@example.com', password: 'secure123' });
expect(response.status).toBe(201);
const user = await db.users.findByEmail('test@example.com');
expect(user).toBeDefined();
expect(emailService.sent).toContainEqual(
expect.objectContaining({ to: 'test@example.com', template: 'welcome' })
);
});
});
This single test covers the API endpoint, validation, database persistence, and email sending. One test, high confidence.
What to E2E Test
Only test critical business paths:
- User signup and login
- Core product workflow (checkout, payment, etc.)
- Permission boundaries (admin vs. user)
E2E tests are slow and flaky. Keep them focused on the paths where a failure means lost revenue or users.
The Pragmatic Rules
-
If a bug reaches production, write a test that would have caught it. This naturally builds coverage where it matters most.
-
Delete tests that break with every refactor but catch no bugs. They’re maintenance cost with no value.
-
Mock at the boundary, not in the middle. Mock the database driver, not your repository layer.
-
Test behavior, not implementation. “When a user submits the form, they see a success message” - not “when handleSubmit is called, setState is invoked with {loading: true}.”
The goal isn’t 100% coverage. It’s maximum confidence with minimum maintenance burden.
Mohan
Software engineer writing about AI, distributed systems, and the craft of building great software.
Related Articles
Distributed Systems Fundamentals Every Developer Should Know
CAP theorem, consensus algorithms, and event-driven architecture - the core concepts behind every scalable system, explained for working developers.
Modern AI Agent Architectures: How Multi-Agent Systems Like OpenHands and Claude Flow Work
A deep dive into multi-agent AI architectures - how frameworks like OpenHands and Claude Flow use planner-worker patterns, role-based orchestration, and parallel execution to solve complex tasks.
TypeScript Patterns That Scale: Lessons from Large Codebases
Discriminated unions, branded types, and the builder pattern - TypeScript techniques that keep large codebases maintainable.
Stay up to date
Get notified when I publish new articles. No spam, unsubscribe anytime.
No spam. Unsubscribe anytime.