A Testing Strategy That Actually Finds Bugs
The 90% Coverage Lie
A client's codebase had 90% test coverage. They were proud of it — it was on their engineering blog. Then they shipped a pricing bug that charged customers double. The code that calculated prices had full coverage. Every test passed. But every test checked the happy path with round numbers. Nobody tested what happened when a 15% discount applied to a $29.97 item with tax.
Coverage tells you what code ran during tests. It doesn't tell you whether the tests actually verified correct behavior.
The Testing Pyramid (Updated for 2026)
The classic pyramid still holds, but the proportions have shifted:
╱╲
╱ ╲ E2E Tests (5-10%)
╱────╲ Critical user flows only
╱ ╲
╱────────╲ Integration Tests (20-30%)
╱ ╲ API contracts, database queries
╱────────────╲
╱ ╲ Unit Tests (60-70%)
╱────────────────╲ Business logic, pure functions
The shift: Integration tests matter more than they used to. Modern apps are API-driven — the integration between your code and the database, third-party APIs, and other services is where most bugs live.
Unit Tests: Test Behavior, Not Implementation
// ✗ Testing implementation (brittle, breaks when you refactor)
test("calculateDiscount calls getPricingTier", () => {
const spy = jest.spyOn(pricing, "getPricingTier");
calculateDiscount(order);
expect(spy).toHaveBeenCalledWith(order.customerId);
});
// ✓ Testing behavior (stable, tests what users care about)
test("applies 15% discount for gold tier customers", () => {
const order = createOrder({ subtotal: 100, customerTier: "gold" });
const result = calculateDiscount(order);
expect(result.discount).toBe(15);
expect(result.total).toBe(85);
});
test("rounds discount to 2 decimal places", () => {
const order = createOrder({ subtotal: 29.97, customerTier: "gold" });
const result = calculateDiscount(order);
expect(result.discount).toBe(4.50); // 15% of 29.97 = 4.4955 → 4.50
expect(result.total).toBe(25.47);
});
test("never discounts below $0", () => {
const order = createOrder({ subtotal: 1.00, discountCode: "100OFF" });
const result = calculateDiscount(order);
expect(result.total).toBe(0);
expect(result.discount).toBe(1.00);
});The behavior tests survive refactoring. The implementation test breaks every time you change internals.
Integration Tests: Where the Real Bugs Are
Unit tests can't catch bugs that happen between components:
// Test the actual database interaction, not a mock
describe("Order Repository", () => {
let db: TestDatabase;
beforeAll(async () => {
db = await TestDatabase.create(); // Real database in Docker
await db.migrate();
});
beforeEach(async () => {
await db.truncate(["orders", "order_items"]);
});
test("creates order with items and calculates total", async () => {
const order = await orderRepository.create({
customerId: "cust_123",
items: [
{ productId: "prod_1", quantity: 2, unitPrice: 29.99 },
{ productId: "prod_2", quantity: 1, unitPrice: 49.99 },
],
});
expect(order.subtotal).toBe(109.97);
expect(order.items).toHaveLength(2);
// Verify it actually persisted
const retrieved = await orderRepository.findById(order.id);
expect(retrieved.subtotal).toBe(109.97);
});
test("handles concurrent inventory deductions", async () => {
await inventory.set("prod_1", 1); // Only 1 in stock
// Two orders trying to claim the last item simultaneously
const [result1, result2] = await Promise.allSettled([
orderRepository.create({ items: [{ productId: "prod_1", quantity: 1 }] }),
orderRepository.create({ items: [{ productId: "prod_1", quantity: 1 }] }),
]);
const successes = [result1, result2].filter(r => r.status === "fulfilled");
expect(successes).toHaveLength(1); // Only one should succeed
});
});Edge Case Testing: Where Bugs Hide
The bugs that reach production live in edge cases:
describe("Price Calculations - Edge Cases", () => {
// Boundary values
test("handles $0 order", () => { /* ... */ });
test("handles max order value ($999,999.99)", () => { /* ... */ });
test("handles single penny ($0.01)", () => { /* ... */ });
// Floating point issues
test("0.1 + 0.2 calculations are correct", () => {
const items = [{ price: 0.1 }, { price: 0.2 }];
expect(calculateTotal(items)).toBe(0.3); // Not 0.30000000000000004
});
// Null/undefined
test("handles missing discount code", () => { /* ... */ });
test("handles empty cart", () => { /* ... */ });
// Race conditions
test("handles duplicate webhook delivery", () => { /* ... */ });
// Timezone issues
test("discount valid until midnight PST applies correctly in UTC", () => { /* ... */ });
});E2E Tests: Critical Paths Only
E2E tests are slow and flaky. Use them sparingly for critical business flows:
// Only test the flows that, if broken, would lose you money
const CRITICAL_FLOWS = [
"anonymous user can browse → add to cart → checkout → receive confirmation",
"logged-in user can reorder from order history",
"user can apply discount code at checkout",
"subscription user can skip/cancel a shipment",
];
test("complete purchase flow", async ({ page }) => {
await page.goto("/products/best-seller");
await page.click("[data-testid=add-to-cart]");
await page.click("[data-testid=checkout]");
await page.fill("[data-testid=email]", "test@example.com");
await page.fill("[data-testid=card]", "4242424242424242");
await page.click("[data-testid=place-order]");
await expect(page.locator("[data-testid=confirmation]")).toBeVisible();
});The Test Prioritization Matrix
When you can't test everything, prioritize:
| Risk Level | What to Test | How |
|------------|-------------------------------------|-------------|
| Critical | Payment, checkout, auth | Unit + Int + E2E |
| High | Order processing, inventory | Unit + Integration |
| Medium | User profiles, preferences | Unit tests |
| Low | Static pages, cosmetic features | Visual regression |
Write tests that would have caught your last 5 production bugs. That's a better starting point than any coverage target. The goal isn't to test everything — it's to test the things that matter before your customers discover they're broken.