What Happens When You Type a URL and Press Enter
DNS lookups, TCP and TLS handshakes, HTTP requests, servers, and the browser's rendering pipeline. The whole journey of a web page, explained so it finally clicks.
Read →Not all tests are equally useful. What each type of test catches, how much of each to write, and a pragmatic testing strategy for web apps that gives confidence without slowing you down.
"We should write more tests" is something every team says. The harder question is which tests. Write the wrong kind and you get a slow, brittle test suite that breaks on every refactor while real bugs slip through. Write the right mix and you can change code with confidence.
Test a single function or module in isolation.
import { calculateTax } from './tax'
test('applies 18% tax to the subtotal', () => {
expect(calculateTax(100, 0.18)).toBe(18)
})
test('rounds to two decimal places', () => {
expect(calculateTax(9.99, 0.18)).toBe(1.8)
})
Best for: pure logic like calculations, parsing, validation, formatting and business rules.
Test several parts working together: your API route with a real database, a service with its repository, a component with its data layer.
test('POST /api/orders creates an order for the logged-in user', async () => {
const user = await createTestUser()
const res = await request(app)
.post('/api/orders')
.set('Cookie', await loginAs(user))
.send({ productId: 42, quantity: 2 })
expect(res.status).toBe(201)
const order = await db.orders.findFirst({ where: { userId: user.id } })
expect(order.quantity).toBe(2)
})
Best for: API endpoints, database queries, authorization rules, anything where wiring matters.
Drive the real app through a browser, like a user would, using tools like Playwright or Cypress.
test('user can sign up and see the dashboard', async ({ page }) => {
await page.goto('/signup')
await page.getByLabel('Email').fill('new@example.com')
await page.getByLabel('Password').fill('a-long-secure-password')
await page.getByRole('button', { name: 'Create account' }).click()
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible()
})
Best for: a handful of critical journeys like signup, login, checkout and the main thing your product does.
The classic testing pyramid says lots of unit tests, fewer integration tests, very few E2E tests. Many web teams now prefer a testing trophy shape that puts the most weight on integration tests, because in web apps most bugs live where pieces connect.
A pragmatic mix for a typical web app:
Mocks replace real dependencies with fakes. They're useful for things you don't control (payment APIs, email services, the current time). But mocking your own code too heavily creates tests that pass while the real integration is broken. Prefer real databases in integration tests (a container or a separate test database) and mock at the edges of your system.
Prioritise tests for:
Chasing 100% coverage isn't the goal. Coverage shows what's untested; it doesn't prove what's tested well.
Tests only protect you if they run on every change. Wire them into CI so a failing test blocks the merge. (GitHub Actions starter.) If you want to go further and write tests first, see test-driven development.
Unit tests check a single function or module in isolation. Integration tests check that several parts, such as an API route and a real database, work correctly together.
Relatively few. Cover the critical user journeys like signup, login and checkout, and rely on faster unit and integration tests for the details.
No. High coverage of critical logic is valuable, but coverage alone doesn't measure test quality. Focus tests on high-risk areas and real behaviour.
DNS lookups, TCP and TLS handshakes, HTTP requests, servers, and the browser's rendering pipeline. The whole journey of a web page, explained so it finally clicks.
Read →There are dozens of HTTP status codes, but about fifteen cover nearly everything. What each one means, when to return it, and the mix-ups (401 vs 403, 301 vs 302) that confuse everyone.
Read →A, AAAA, CNAME, MX, TXT and NS records in plain English, why DNS changes seem slow, and the troubleshooting commands that tell you what is really going on.
Read →