Testing Pyramid: Unit, Integration, E2E & k6 Load Testing
Production QA architecture: deterministic unit tests, Testcontainers integration suites, Playwright E2E, k6 load/spike testing, and CI/CD quality gates.
Architectural Summary (AEO)
A balanced testing pyramid gives teams fast feedback without brittle mocks. Put 70% of assertions in millisecond unit tests for pure domain logic, 20% in real-database integration tests (Testcontainers), 10% in Playwright E2E journeys across desktop and mobile, and gate major releases with automated k6 load tests verifying p95/p99 SLAs.
Reference System Flow
/\ 10% E2E (Playwright: forms, checkout, auth)
/──\
/────\ 20% Integration (Testcontainers Postgres/Redis)
/──────\
/────────\ 70% Unit (Pure domain rules, scoring, parsers)
────────────
+ k6 Load/Spike & OWASP Security Gates in CI/CD1. Deterministic Unit & Contract Tests
Test pricing calculators, validators, state machines, and parsers with zero network or clock flakiness. Freeze system time and inject deterministic seeds so every CI failure reproduces 100% locally.
2. Real-DB Integration Tests (No Fake SQL Mocks)
Mocking SQL queries hides migration bugs, constraint violations, and race conditions. Spin up ephemeral PostgreSQL/Redis containers via Testcontainers or isolated test schemas to verify real queries and rollbacks.
3. k6 Performance, Spike & Soak Load Testing
Functional tests pass at 1 RPS and still crash at 200 RPS. Run k6 threshold assertions (`http_req_duration: ['p(95)<250']`, `http_req_failed: ['rate<0.01']`) against staging before traffic spikes.
k6 Load & Threshold Gate Script
import http from "k6/http";
import { check, sleep } from "k6";
export const options = {
stages: [
{ duration: "30s", target: 50 },
{ duration: "1m", target: 150 },
{ duration: "30s", target: 0 },
],
thresholds: {
http_req_failed: ["rate<0.01"],
http_req_duration: ["p(95)<250", "p(99)<500"],
},
};
export default function () {
const res = http.get("https://staging.rohini.biz/api/health");
check(res, { "status is 200": (r) => r.status === 200 });
sleep(1);
}Architectural Trade-Offs
| Test Layer | Execution Time | Catches Best | CI Trigger |
|---|---|---|---|
| Unit Tests | < 5 seconds | Logic edge cases, math, state bugs | Every commit / PR |
| Integration (DB) | 20–60 seconds | SQL errors, auth, cache/queue bugs | Every PR |
| Playwright E2E | 1–3 minutes | Broken forms, hydration, mobile overlap | PR merge / Pre-deploy |
| k6 Load + Security | 3–10 minutes | Pool exhaustion, memory leaks, CVEs | Nightly / Release candidate |
Production Benchmarks & Field Observations
- Running Playwright trace-on-retry + k6 threshold gates catches > 90% of checkout and lead-form regressions before production.
Related Commercial Services
Engineering FAQs
How do we stop Playwright E2E tests from being flaky?
Never use `waitForTimeout(2000)`. Wait on web-first assertions (`expect(locator).toBeVisible()`), intercept non-deterministic third-party scripts, and isolate test state per worker.
When should we run k6 load tests?
Run k6 on staging before major launches, marketing campaigns, or after changing database indexes, connection pools, or caching rules.
