Every team says it values automated testing; far fewer can ship to production on a green pipeline without a human double-checking. The difference is not which test types you can name — it is how the suite is shaped, what it runs against, and whether the pipeline is allowed to say no. Here is the strategy we apply in practice.

The Test Pyramid Still Works

The classic shape remains the right default: many fast unit tests at the base, a smaller layer of integration tests, and a thin top of end-to-end tests. The reasoning is economic. A unit test with Vitest, JUnit 5, or pytest runs in milliseconds and pinpoints the failing function; an end-to-end test takes minutes, can fail for a dozen unrelated reasons, and tells you only that something broke. Teams that invert the pyramid — hundreds of browser tests, few unit tests — end up with pipelines that take an hour and get ignored. Regression protection falls out of this for free: your existing suite, run on every change, is what verifies that old behavior still holds. It cannot guarantee compatibility; it can only catch the break before your users do — which is the point.

Integration Tests Against Real Dependencies

The most common testing lie is a mocked database. Mocks verify that your code calls the driver the way you assumed — not that the query works. Testcontainers changed this economics: your test suite spins up a real PostgreSQL, Redis, or Kafka in Docker, runs against it, and tears it down, both locally and in CI. Reserve mocks for boundaries you genuinely cannot control, like third-party APIs — and cover those with contract tests instead.

Contract Testing for Service Boundaries

In a microservices estate, the alternative to contract testing is a shared staging environment where every service’s end-to-end suite runs against every other service — slow, flaky, and quadratically expensive. Consumer-driven contract testing with Pact inverts it: each consumer publishes the exact requests and responses it depends on, and each provider verifies those contracts in its own pipeline. A provider that breaks a consumer finds out at build time, in isolation, without a staging environment involved.

End-to-End, Sparingly

End-to-end tests with Playwright (or Cypress) earn their cost only on the journeys where a failure is an incident: sign-up, login, checkout, the core workflow of your product. Keep the count in the dozens, not the hundreds, run them against a production-like environment, and resist the urge to reach for a browser test when a unit test would catch the same bug three layers cheaper.

Shift Left: Quality Gates in CI

Shift-left means the checks run on the pull request, before merge — not nightly, not in a hardening sprint. Concretely, that is a CI pipeline where lint, unit tests with a coverage threshold, and integration tests are all required status checks, and merging is blocked until they pass. A minimal GitHub Actions version:

name: ci
on:
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: actions/setup-node@v5
        with:
          node-version: 22
          cache: npm
      - run: npm ci
      - run: npm run lint
      - run: npm test -- --coverage
      - run: npm run test:integration

The gate matters more than the tooling: if a red pipeline can be merged anyway, you do not have quality gates — you have suggestions. Treat coverage thresholds as a floor against erosion, not a target to game; 80% with meaningful assertions beats 95% of tests that assert nothing.

Managing Flaky Tests

Flaky tests are not an annoyance; they are corrosion. Once engineers learn that re-running fixes failures, they re-run real failures too. The working policy is: detect flakes by tracking pass-after-retry rates in CI, quarantine them immediately into a non-blocking job so the main pipeline stays trustworthy, and give quarantined tests an owner and a deadline — fixed or deleted. Automatic retries are acceptable only as a detection mechanism, never as a permanent workaround.

Conclusion

A working strategy is unglamorous: a pyramid-shaped suite, integration tests against real dependencies, contracts at service boundaries, a handful of end-to-end journeys, hard gates in CI, and zero tolerance for flakes. Get those right and deployment stops being an event. If your pipeline is not there yet, our DevOps practice builds exactly this kind of delivery machinery.