A Pragmatic Guide to Testing Web Applications: What to Test and How
Testing is one of those topics every team agrees is important, yet few approach with a clear plan. At TCCB Solutions, we've shipped enough applications to know that a thoughtful testing strategy isn't about chasing 100% coverage — it's about buying confidence where it matters most. Here's how we think about it.
The Testing Pyramid, Revisited
The classic testing pyramid still holds up. We write many fast unit tests, some integration tests, and a few end-to-end tests. The reasoning is simple: unit tests are cheap to write and run in milliseconds, while end-to-end tests are slow and brittle. We lean on the bottom of the pyramid for breadth and reserve the top for the handful of user journeys that absolutely cannot break.
- Unit tests — pure functions, business logic, edge cases
- Integration tests — how modules, databases, and APIs work together
- End-to-end tests — critical paths a real user takes, in a real browser
Unit Tests: Cover the Logic, Not the Framework
The best unit tests target the code you wrote — the calculations, validations, and transformations that carry your business rules. We don't waste effort testing that React renders a <div> or that an ORM saves a row; we test the logic around them. A good unit test reads like a specification:
import { describe, it, expect } from 'vitest';
import { calculateDiscount } from './pricing';
describe('calculateDiscount', () => {
it('applies a 10% discount over $100', () => {
expect(calculateDiscount(150)).toBe(135);
});
it('never returns a negative price', () => {
expect(calculateDiscount(0)).toBe(0);
});
});
Notice the second case. We always ask, "what's the weird input?" — empty values, zeros, negatives, and boundaries are where bugs hide.
Integration Tests: Trust, but Verify
Unit tests can all pass while your app is still broken, because the seams between components are where things actually fall apart. Integration tests exercise those seams — typically an API endpoint talking to a real (or in-memory) database. For a Node API, we reach for supertest:
import request from 'supertest';
import { app } from '../app';
it('creates a user and returns 201', async () => {
const res = await request(app)
.post('/api/users')
.send({ email: '[email protected]' });
expect(res.status).toBe(201);
expect(res.body.id).toBeDefined();
});
These tests catch the mismatches no unit test can: a wrong status code, a missing migration, a serialization quirk. We run them against a disposable database so every run starts clean.
End-to-End Tests: Protect the Money Paths
We don't try to E2E-test everything — that way lies a slow, flaky suite everyone learns to ignore. Instead, we identify the journeys that directly affect the business: signing up, logging in, checking out. We automate those with Playwright, which drives a real browser:
import { test, expect } from '@playwright/test';
test('user can log in', async ({ page }) => {
await page.goto('/login');
await page.fill('#email', '[email protected]');
await page.fill('#password', 'secret');
await page.click('button[type="submit"]');
await expect(page).toHaveURL('/dashboard');
});
Don't Forget the Non-Functional Stuff
Functional correctness is only half the picture. Before we call an application production-ready, we also check:
- Accessibility — automated audits with tools like axe, plus keyboard navigation
- Performance — Lighthouse budgets so a page doesn't quietly get slower over time
- Security — dependency scanning and tests for auth and input validation
Make It Automatic
A test suite only earns its keep when it runs on every change. We wire tests into CI so that nothing merges with a red build. The goal isn't to slow developers down — it's to let them move faster, knowing a safety net is in place. A quick failing test in CI is far cheaper than a 2 a.m. incident in production.
Start Small, Be Consistent
If your project has no tests today, don't try to boil the ocean. Add a test for the next bug you fix, then the next feature you build. Confidence compounds. Over time you'll have a suite that reflects how your application actually behaves — and the freedom to refactor without fear.
Not sure where to begin, or want a second opinion on your current setup? We'd love to help you build a testing strategy that fits your team and your codebase. Get in touch with us and let's talk.