Deploying software to production without automated tests is like driving on a mountain road without brakes.
As a codebase grows, every new feature risks breaking an existing invoice calculation, permission check, or checkout flow.
Here is how high-craft engineering teams structure their automated testing pyramid.
One honest preamble: the pyramid is a heuristic, not a law. Its useful content is the cost gradient — unit tests are cheap and fast, end-to-end tests are expensive and slow, so the bulk of your confidence should come from the cheap end. What you test, and how strictly, should follow the risk in your business logic rather than a fixed ratio.
The Practical Testing Pyramid
▲
/ \
/ \ E2E Tests (Playwright) ~10%
/─────\ Critical journeys: signup, checkout, billing
/ \
/─────────\ Integration & API Tests (pytest + DRF) ~30%
/ \ Endpoints, permissions, webhooks, integrations
/─────────────\
/ \ Unit Tests (pytest) ~60%
/─────────────────\ Domain rules, calculations, validators
Key Testing Principles
- Test Business Rules, Not Framework Code: Don't test if Django saves a field; test that an invoice correctly applies Australian GST or European VAT taxes.
- Use Factory Boy Instead of Static Fixtures: Generate realistic mock data dynamically for faster, maintainable test suites.
- Automate in GitHub Actions: Never merge code into
mainunless 100% of test suites pass automatically in CI.
A Fourth Principle Worth Adding: Every Bug Earns a Test
The single highest-value testing habit is not coverage targets; it is this policy: whenever a bug reaches production, the fix ships with a test that fails without it. It costs a few minutes per incident, requires no upfront buy-in, and over a year produces a suite that mirrors the actual failure modes of your specific product — which is far more valuable than a suite written to satisfy a percentage.
What Each Level Should Actually Cover
| Level | Test this | Do not test this |
|---|---|---|
| Unit | Tax and currency math, discount rules, permission predicates, date/timezone logic, state machines | Framework internals, trivial getters |
| Integration / API | Endpoint permissions, validation errors, pagination, webhook signature rejection, idempotency | Every field of every serializer |
| Contract | OpenAPI schema compatibility between DRF and the frontend | Implementation details |
| E2E | Signup → first value, checkout → invoice, core workflow per role | Every form validation path |
| Non-functional | Load on key endpoints occasionally, dependency scanning in CI | Full-scale load test on every commit |
Writing Tests That Stay Useful
Structure: Arrange–Act–Assert, one behaviour per test. A test named test_that_the_app_works protects nothing. test_annual_plan_prorates_upgrade_mid_cycle documents a rule and fails meaningfully when that rule changes.
Test the seam, not the internals. Assert on observable behaviour — the returned value, the database state, the HTTP response — rather than private method calls. Refactoring should not break tests; only behaviour changes should.
Mock at the boundary. External systems (Stripe, carrier APIs, SMTP) belong behind a thin adapter, and tests should replace that adapter — not patch requests.get in twelve places. responses or respx for HTTP, a local fake for vendor SDKs, and a hard rule that no test makes a real network request. A test suite that hits live APIs is slow, flaky, and occasionally charges real cards.
Freeze time rather than sleeping. freezegun for date logic; time.sleep in tests means something is wrong with the design. Timezone tests are worth writing deliberately for businesses operating across the US, Europe, Australia and South Asia — "yesterday" is not the same day in all of them, and month-boundary bugs are expensive.
Keep test data deterministic. Factory Boy plus a fixed Faker seed gives realistic data and reproducible failures. A test that fails one run in ten because of random values is worse than no test, because the team learns to re-run rather than investigate.
Speed: The Difference Between a Suite People Run and One They Avoid
| Technique | Effect |
|---|---|
pytest-xdist parallel execution | 3–6× faster suites |
--reuse-db between runs | Removes per-run migration cost |
Transaction rollback per test (pytest-django default) | Isolated tests with no teardown cost |
| No network calls in unit tests | Seconds saved per test, flakiness removed |
| Split suites: unit on every push, E2E on pull requests | Fast feedback where it matters |
| Browser reuse in Playwright (one context per worker) | Large E2E speedups |
A rule of thumb worth defending: the unit suite should complete in under 90 seconds on a laptop. Beyond that, developers stop running it locally, and the only feedback loop left is CI — which is much too slow for the way people actually work.
Flaky Tests: Handle Them Like Production Incidents
Flakiness destroys trust in a suite faster than missing tests do. The policy that works:
- Detect: record which tests fail and pass on retry. Any test that is not deterministic gets flagged automatically.
- Quarantine: move it out of the blocking pipeline so it does not block other work — but keep running it.
- Fix within a week, with a deadline and an owner. Most flakiness comes from a handful of causes: timing assumptions, animation-dependent selectors, shared state between tests, test order dependence, and unsorted results.
- Never "fix" it with automatic retries in CI. Retries hide real regressions. If a test needs three attempts to pass, it is testing nothing.
CI Configuration and Quality Gates
A sensible pipeline for a Django + Nuxt stack:
- On every push: lint, type checks, unit tests, migration checks (
makemigrations --checkto catch models changed without a migration). - On pull request: API/integration tests, contract test against the OpenAPI schema, frontend unit tests, E2E on the critical journeys.
- On merge to main: deploy to staging, run the E2E suite against staging, then release.
- Weekly: dependency vulnerability scan, dependency upgrade PR, a light load test on the endpoints that matter.
Two gates that catch the most damaging classes of bug in our experience: the contract test (a removed API field breaks the frontend silently, and only a schema test notices) and makemigrations --check (a model change without a migration looks fine locally and fails in production).
Artifacts matter too: keep Playwright traces, screenshots and videos for failed runs. A failure that takes 40 minutes to reproduce locally is usually diagnosed in two minutes from a trace.
What About the Frontend?
A Django + Nuxt codebase needs frontend tests too, but far fewer than most teams write. The useful split:
- Vitest for composables, stores, formatters and permission helpers — the logic that would otherwise only be verified by clicking around.
- Vue Test Utils for components with real behaviour: multi-step forms, validation display, conditional rendering based on permissions.
- Accessibility checks with
axein component tests, which catch missing labels and contrast regressions automatically. - Playwright reserved for the handful of journeys that span the whole stack.
Skip snapshot tests of large components. They fail on every cosmetic change, get approved without reading, and end up protecting nothing but the snapshot file itself.
What Automated Tests Cannot Do
Being clear about the limits keeps the suite honest:
- They do not catch wrong requirements. If the tax rule was misunderstood, the test asserts the wrong behaviour faithfully.
- They do not judge design or copy. Your tests will not tell you the onboarding flow is confusing.
- They do not replace monitoring. Uptime, error rates and queue depth are production facts, not test outcomes.
- They do not prove security. A passing permission test proves one path was checked; only a review and adversarial testing covers the rest.
Pair automated tests with short exploratory testing on risky changes, and keep the suite focused on the behaviour that costs money when it breaks.
Introducing Tests Into a Codebase That Has None
Most teams inheriting a legacy application start here, and the temptation is to stop feature work for a month and write tests. That approach usually fails: the suite is written against behaviour nobody has questioned, the sprint overruns, and confidence in testing drops.
A sequence that works better:
- Week 1: add the safety net around money and access. Write integration tests for the endpoints that touch invoices, payments and permissions — the paths where a bug is most expensive. Ten to twenty well-chosen tests already cover a large share of the risk.
- Week 2: install the pipeline. Get tests running in CI on every pull request, even if the suite is small. A suite that only runs locally is a suite that stops running.
- Ongoing: the bug-earns-a-test rule. Every production fix adds a regression test. Within two quarters this produces a suite shaped by reality.
- Add a test as part of each feature, scoped to the new behaviour rather than retrofitting the whole module.
- Refactor with coverage. When you touch a module, bring its critical paths under test as you go — the refactor gives you a natural reason to write the tests, and the tests protect the refactor.
What not to do: chase a coverage percentage, test generated code, or write a hundred E2E tests because they look impressive in a dashboard. Coverage tells you what is executed, not what is verified, and a suite full of assertions that only check for HTTP 200 verifies almost nothing.
Six Principles to Take Away
- Test business rules, not framework behaviour.
- Prefer factories over fixtures; keep data deterministic.
- Every production bug earns a regression test.
- Mock at the boundary — never let tests call the open internet.
- Keep the fast suite under 90 seconds so people actually run it.
- Treat flaky tests as incidents, not inconveniences.
Related reading: DevOps and CI/CD is where these tests run; technical debt covers the codebases that arrive without them; and background tasks covers testing the asynchronous half. Ship reliable, rock-solid software backed by rigorous automated testing. Consult with KEHEM IT.
No tests and afraid to deploy? Talk to us about adopting testing on an existing codebase.
Have a project in mind?
KEHEM designs and builds thoughtful websites, SaaS products, and business systems.