I see this pattern at least twice a quarter: a company with 10-15 engineers, growing fast, shipping constantly, and every deploy is a coin flip. No tests, or tests that were written once and never maintained, or tests that pass locally and fail in CI. The team is terrified of Friday deploys. They should be — they're deploying blind.

The temptation is to declare "testing month" and try to retrofit coverage across the entire codebase. Don't do that. It's expensive, demoralizing, and most of the tests you write will be for code that works fine and never changes.

The Pragmatic Testing Pyramid

You've probably seen the testing pyramid. Unit tests at the base (many, fast, cheap), integration tests in the middle (fewer, slower, more realistic), and end-to-end tests at the top (fewest, slowest, most brittle). The framework is right. The implementation is where most teams go wrong.

Unit Tests: Business Logic Only

Don't unit test everything. Unit test the code that makes decisions: pricing calculations, permission checks, data transformations, validation rules. This is the code where a bug means wrong numbers, wrong access, or corrupted data. It's also the code that's cheapest to test — pure functions with clear inputs and outputs.

Skip unit tests for: glue code that just passes data between systems, simple CRUD operations, UI layouts (unless the UI implements business logic), and configuration files. These are better caught by integration tests or manual review.

Target: new business logic should have unit tests before it merges. Legacy business logic gets tests when you modify it. Don't backfill tests for stable code that works.

Integration Tests: Critical Paths

These test that your components actually work together. Not everything — the critical paths that, if broken, would directly impact customers or revenue.

For most SaaS applications, the critical paths are: user authentication and authorization, payment processing and billing, core data creation and retrieval (the main thing your app does), and any external integrations (email delivery, webhook processing, third-party APIs).

Integration tests should hit real databases, real queues, and real (sandbox) external services. Mocked integration tests give you false confidence — they test that your mock works, not that your integration works. Use Docker Compose or Testcontainers to spin up dependencies in CI.

End-to-End Tests: The Golden Paths

End-to-end tests are expensive to write, slow to run, and fragile to maintain. Write as few as possible — but write the ones that matter.

Identify your 5-10 "golden paths" — the user journeys that represent 80% of your business value. New user signup, core feature usage, payment flow, settings changes. Automate these with Playwright or Cypress. Run them on every deploy.

If an end-to-end test starts flaking — failing intermittently for non-obvious reasons — fix it immediately or delete it. A flaky test that the team learns to ignore is worse than no test at all, because it trains people to ignore failures.

Building the Culture

The technical strategy matters less than the cultural shift. Here's what actually changes behavior:

Tests block the PR. If a PR modifies business logic and doesn't include tests, it doesn't merge. This single rule changes everything. Engineers start writing tests not because they believe in testing philosophy, but because they can't ship without them. After a few months, it becomes habit.

Fast CI is non-negotiable. If your test suite takes 30 minutes, developers will context-switch while waiting and lose the feedback loop. Target under 10 minutes for the full suite. Parallelize tests, use faster test databases, and cache dependencies aggressively.

Celebrate catches. When a test catches a real bug before production, call it out in standup. "The billing integration test caught a regression in yesterday's PR that would have double-charged customers." This builds trust that testing is worth the investment.

Don't track coverage percentage as a metric. Coverage metrics incentivize testing easy code to hit a number, not testing important code to prevent incidents. Track something meaningful instead: "number of production incidents caused by code changes" should be trending down. That's the metric that shows your testing strategy is working.

The Legacy Codebase Problem

If you have a large untested codebase, the strategy is simple: don't test backward, test forward. Every new feature and every bug fix gets tests. When you need to modify legacy code, write characterization tests first — tests that document the current behavior, even if that behavior is weird.

Over 6-12 months, the most-modified code accumulates test coverage naturally, and the stable code that nobody touches stays untested (which is fine, because nobody's touching it).


Related: DevOps Fundamentals for Growing Teams, Inherited Codebase: What to Do First, Engineering Metrics That Matter