Unit vs Integration vs E2E Mapping
Architectural Mapping & Framework Integration
Establishing a deterministic mapping matrix across Jest/Vitest, Playwright/Cypress, and React Testing Library requires strict contract boundaries before toolchain configuration begins. Layer responsibilities must be explicitly defined to prevent assertion overlap and execution bloat. Align your foundational architecture with Modern JavaScript Test Strategy & Pyramid Design to ensure each tier validates a single, non-overlapping concern.
Actionable Implementation Patterns
1. Shared Fixture Injection via Dependency Injection Containers Avoid global state pollution by injecting deterministic fixtures through a lightweight DI container. This guarantees isolation across unit and integration boundaries.
// test/fixtures/container.ts
import { Container } from 'inversify';
import { TYPES } from '../../src/di/types';
import { AuthService } from '../../src/services/auth';
import { MockAuthService } from './mocks/auth';
export const createTestContainer = (overrides: Partial<Record<TYPES, unknown>> = {}) => {
const container = new Container();
container.bind(TYPES.AuthService).to(overrides[TYPES.AuthService] || MockAuthService);
return container;
};
2. MSW Boundary Isolation for Integration Layers
Intercept network requests at the worker level rather than mocking fetch/axios directly. This preserves native browser behavior while isolating external dependencies.
// test/mocks/server.ts
import { setupServer } from 'msw/node';
import { handlers } from './handlers';
export const server = setupServer(...handlers);
// test/setup.ts
import { server } from './mocks/server';
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
CI Pipeline Rules
- Workspace-Scoped Execution: Enforce targeted runner allocation using Nx or Turborepo to prevent full-suite execution on isolated changes.
nx affected:test --base=main --head=HEAD --only=unit
- Flakiness Threshold Enforcement: Block merges if integration test flakiness exceeds 5% over a rolling 30-day window. Implement via a custom CI script parsing historical test reports.
Debugging Workflows
- Selector Contract Tracing: When an E2E test fails on a missing element, trace the selector back to the component’s integration contract. Verify the DOM structure matches the RTL render output before inspecting Playwright/Cypress locators.
- Network Interception Validation: Enable Playwright API debugging to verify MSW boundaries:
DEBUG=pw:api npx playwright test --grep @integration
Configuration Steps & Environment Parity
Environment variable propagation, ephemeral database seeding, and CI runner allocation dictate execution parity. Parallel execution overhead must be justified by evaluating resource distribution against defect detection velocity, as detailed in the Cost-Benefit Analysis of Test Layers.
Actionable Implementation Patterns
1. Dockerized Ephemeral DB Containers with Deterministic Seeds Spin up isolated PostgreSQL/MySQL instances per CI job. Use deterministic seed scripts to guarantee identical data states across runs.
# test/docker/Dockerfile.db
FROM postgres:15-alpine
COPY test/fixtures/seed.sql /docker-entrypoint-initdb.d/
ENV POSTGRES_DB=test_db POSTGRES_USER=test POSTGRES_PASSWORD=test
# docker-compose.test.yml
version: '3.8'
services:
db:
build: ./test/docker
ports: ["5432:5432"]
environment:
- POSTGRES_DB=test_db
2. Vite/Vitest Config Splitting via defineConfig
Separate configuration transforms to prevent heavy integration plugins from polluting unit test execution.
// vitest.config.ts
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
export default defineConfig(({ mode }) => {
const isIntegration = mode === 'integration';
return {
plugins: isIntegration ? [react()] : [],
test: {
globals: true,
environment: isIntegration ? 'jsdom' : 'node',
include: isIntegration ? ['**/*.integration.test.ts'] : ['**/*.unit.test.ts'],
},
};
});
CI Pipeline Rules
- Binary & Dependency Caching: Cache
node_modulesand browser binaries at the pipeline level to eliminate redundant downloads.
- name: Cache Playwright Browsers
uses: actions/cache@v3
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
- Matrix Builds for Node.js LTS: Execute unit and integration suites across supported LTS versions to catch runtime-specific regressions.
strategy:
matrix:
node-version: [18.x, 20.x, 22.x]
Debugging Workflows
- Local CI Reproduction: Reproduce pipeline failures locally using identical containerized runners to eliminate environment drift.
docker compose -f docker-compose.test.yml up -d
NODE_ENV=test npx vitest run --mode=integration
- Execution Bottleneck Isolation: Parse JSON reporter output to identify slow suites:
npx vitest run --reporter=json | jq '.testResults[] | select(.duration > 5000) | {file: .name, duration: .duration}'
Reliability Tradeoffs & Coverage Thresholds
Flakiness directly erodes execution velocity and developer trust. Coverage metrics must map to business risk rather than arbitrary percentages. Align your threshold definitions with Defining Coverage Thresholds to prevent metric-driven false confidence and enforce meaningful validation gates.
Actionable Implementation Patterns
1. Exponential Backoff Retry Logic for Network-Dependent E2E Flows Implement deterministic retries for transient network failures without masking genuine defects.
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
retries: process.env.CI ? 2 : 0,
use: {
trace: 'on-first-retry',
actionTimeout: 15000,
},
});
2. Contract Testing (Pact) to Replace Brittle Integration Mocks Shift from hardcoded mock responses to consumer-driven contracts. This guarantees API compatibility before deployment.
// test/pact/consumer.spec.ts
import { PactV3, MatchersV3 } from '@pact-foundation/pact';
import { fetchUser } from '../../src/api/user';
const pact = new PactV3({ consumer: 'WebApp', provider: 'UserAPI' });
pact
.given('user exists')
.uponReceiving('a request for user profile')
.withRequest({ method: 'GET', path: '/users/123' })
.willRespondWith({ status: 200, body: { id: '123', name: MatchersV3.string() } })
.executeTest(async (mockServer) => {
const user = await fetchUser(mockServer.url, '123');
expect(user.name).toBeDefined();
});
CI Pipeline Rules
- Auto-Quarantine Logic: Automatically flag and quarantine tests after 3 consecutive failures. Route quarantined tests to a low-priority nightly suite until root-cause resolution.
- Coverage Gate Enforcement: Enforce strict minimums aligned to layer risk profiles:
// package.json
"jest": {
"coverageThreshold": {
"global": {
"branches": 85,
"functions": 90,
"lines": 90,
"statements": 90
},
"src/integration/": {
"branches": 65,
"functions": 70,
"lines": 70
},
"e2e/": {
"lines": 30
}
}
}
Debugging Workflows
- Race Condition Isolation: Force sequential execution and freeze timers to eliminate async race conditions:
npx vitest run --run-in-band --fake-timers
- Snapshot Drift Auditing: Update snapshots only after manual review gates. Use
--updateSnapshotin a dedicated PR branch, never in CI.
Strategic ROI & Test Layer Allocation
Quantify maintenance overhead against defect detection rates to optimize resource allocation. Calculate infrastructure spend using How to calculate ROI for E2E tests in React apps to validate execution frequency and justify CI runner scaling.
Actionable Implementation Patterns
1. Shift-Left Critical Path Validation Move critical business logic assertions from E2E to the integration layer. E2E should only verify routing, hydration, and end-to-end user flows.
// src/features/checkout/integration.test.ts
describe('Checkout Flow Integration', () => {
it('calculates tax and total correctly', async () => {
const { result } = renderHook(() => useCheckoutCart());
result.current.addItem({ id: '1', price: 100, taxRate: 0.08 });
expect(result.current.total).toBe(108);
});
});
2. Decouple UI Rendering from Business Logic Verification Separate component rendering tests from pure logic validation to reduce E2E maintenance burden.
CI Pipeline Rules
- Deployment Gating: Block production deployments if the E2E smoke suite does not complete within a 15-minute SLA.
- name: E2E Smoke Gate
timeout-minutes: 15
run: npx playwright test --grep @smoke
on-failure: cancel-deployment
- Auto-Scale CI Runners: Dynamically provision runners based on queue depth and historical duration metrics using GitHub Actions or GitLab auto-scaling configurations.
Debugging Workflows
- Telemetry Correlation: Map production error telemetry (Sentry, Datadog) to missing E2E assertions. Prioritize test creation for unvalidated error paths.
- Step-by-Step DOM Inspection: Enable Playwright tracing to capture network, console, and DOM state at every interaction step:
npx playwright test --trace on
# View trace: npx playwright show-trace trace.zip
Optimization & Layer Skipping Criteria
Identify scenarios where integration overhead outweighs defect detection value. Apply When to skip integration tests in favor of unit tests for stateless utilities, pure functions, and isolated data transformations.
Actionable Implementation Patterns
1. Replace Heavy DB Integration with In-Memory SQLite or Mock Repositories Eliminate external DB dependencies for data-access layer validation.
// test/mocks/db.ts
import { Database } from 'better-sqlite3';
export const createInMemoryDB = () => {
const db = new Database(':memory:');
db.exec(`CREATE TABLE users (id TEXT PRIMARY KEY, email TEXT)`);
return db;
};
2. Deploy Property-Based Testing for Edge Case Coverage
Use fast-check to generate thousands of deterministic inputs, covering edge cases without E2E overhead.
// test/property/formatter.spec.ts
import fc from 'fast-check';
import { formatCurrency } from '../../src/utils/formatter';
describe('formatCurrency property tests', () => {
it('always returns valid currency string', () => {
fc.assert(
fc.property(fc.float({ min: 0, max: 10000 }), (amount) => {
const result = formatCurrency(amount);
expect(result).toMatch(/^\$[\d,]+\.\d{2}$/);
})
);
});
});
CI Pipeline Rules
- Path-Based Suite Skipping: Skip integration suites for documentation-only or configuration-only PRs.
paths-ignore:
- 'docs/**'
- '*.md'
- 'config/**'
- Strict Test Tagging: Enforce
@smoke,@regression, and@integrationtags for selective execution and reporting.
npx vitest run --grep "@smoke"
Debugging Workflows
- Mock Fidelity Validation: Run OpenAPI schema diffs against production endpoints to ensure MSW/Pact mocks remain synchronized with actual API contracts.
npx @apidevtools/swagger-cli validate --strict openapi.yaml
- Memory Leak Profiling: Profile long-running test runners to detect unhandled promise rejections or detached DOM nodes:
NODE_OPTIONS="--expose-gc" npx vitest run --logHeapUsage
How to calculate ROI for E2E tests in React apps
Return on Investment for end-to-end testing is not measured by coverage percentages. It is a deterministic function of defect containment, pipeline...
When to skip integration tests in favor of unit tests
Integration tests provide critical validation across architectural boundaries, but they frequently become the primary source of CI pipeline instability,...