This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

Test Types

Definitions of the test types used throughout this site.

Definitions for the test types used throughout this site. Each page covers what the type is, when it runs in the pipeline, what it asserts on, and what it does not.

The list isn’t exhaustive and the boundaries between types aren’t crisp in every codebase. Use these definitions as shared vocabulary for the rest of the testing section, especially Applied Testing Strategies and Testing Antipatterns.

1 - Component Tests

Deterministic tests that exercise a single component through its public interface, with systems the team doesn’t control replaced by test doubles.
Component test pattern: a test actor hits the public interface of a component boundary. Inside the boundary, real internal modules (API Layer, Business Logic, Data Adapter) are wired together. Outside the boundary, a Database and External API are represented by test doubles.

Definition

Verification of a coherent structural unit (such as an entire microservice, UI component, or self-contained subsystem) against its specific contract and internal logic, while keeping interactions beyond that component’s boundary mocked or stubbed.

Scope & Boundaries

Broader than a unit test, but strictly narrower than an end-to-end (E2E) integration test. It tests the interplay of multiple internal classes/modules working together within that component. Out-of-process network calls and external downstream services are replaced by API wire-level stubs or in-memory equivalents (e.g., WireMock, MSW, ephemeral test containers).

Core Characteristics

Validates state management, internal workflows, data transformations, and edge-to-edge behavior within a bounded context without taking dependencies on third-party uptime or network latency.

Good Practices

  • Mock only at boundary borders: Exercise the component’s internal routing, controllers, domain models, and data mappers together; only mock external HTTP APIs, message brokers, or remote databases.
  • Use ephemeral infrastructure: Use fast, disposable local resources (e.g., local SQLite/Postgres in Docker, local WireMock) to mirror real component runtime characteristics.
  • Versioned, repeatable test data.
  • Verify contract-to-state workflows: Validate that boundary inputs result in the correct local state changes and expected outgoing network payloads.

Anti-Patterns

  • E2E scope creep: Allowing the test to call live third-party services or dependent microservices instead of wire-level stubs.
  • Re-testing granular unit logic: Writing dozens of micro-permutations of input edge cases at the component level instead of covering them in fast unit tests.
  • Leaky test harness state: Failing to purge in-memory databases or reset wire stubs between runs, leading to non-deterministic test flakiness.

When to Avoid

They overlap heavily with other layers when the component is:

  • Thin CRUD with no middleware to speak of. Provider contract verification against a booted app plus sociable unit tests of the domain cover most of what a component test would. Keep one per critical flow as smoke coverage; skip exhaustive component coverage.
  • Utility libraries are effectively multiple small components in as a single consumable dependency.
  • Pure transformation logic. Parsers, calculators, scheduling math. Unit tests give better coverage per unit of effort.

If you’re choosing between an extra component test and an extra unit test for the same behavior, the unit test is cheaper to write, run, and maintain. Component tests earn their keep at the seams between layers, not in repeating ground that unit tests already cover.

Examples

Backend Service

A component test for a REST API, exercising the full application stack with the downstream inventory service replaced by a test double:

Backend component test - order creation with stubbed inventory service
describe("POST /orders", () => {
  it("should create an order and return 201", async () => {
    // Arrange: mock the inventory service response
    httpMock("https://inventory.internal")
      .onGet("/stock/item-42")
      .reply(200, { available: true, quantity: 10 });

    // Act: send a request through the full application stack
    const response = await request(app)
      .post("/orders")
      .send({ itemId: "item-42", quantity: 2 });

    // Assert: verify the public interface response
    expect(response.status).toBe(201);
    expect(response.body.orderId).toBeDefined();
    expect(response.body.status).toBe("confirmed");
  });

  it("should return 409 when inventory is insufficient", async () => {
    httpMock("https://inventory.internal")
      .onGet("/stock/item-42")
      .reply(200, { available: true, quantity: 0 });

    const response = await request(app)
      .post("/orders")
      .send({ itemId: "item-42", quantity: 2 });

    expect(response.status).toBe(409);
    expect(response.body.error).toMatch(/insufficient/i);
  });
});

Frontend Component

A component test exercising a login flow with a stubbed authentication service:

Frontend component test - login flow with stubbed auth service
describe("Login page", () => {
  it("should redirect to the dashboard after successful login", async () => {
    mockAuthService.login.mockResolvedValue({ token: "abc123" });

    render(<App />);
    await userEvent.type(screen.getByLabelText("Email"), "ada@example.com");
    await userEvent.type(screen.getByLabelText("Password"), "s3cret");
    await userEvent.click(screen.getByRole("button", { name: "Sign in" }));

    expect(await screen.findByText("Dashboard")).toBeInTheDocument();
  });
});

Accessibility Verification

Component tests already exercise the UI from the actor’s perspective, making them the natural place to verify that interactions work for all users. Accessibility assertions fit alongside existing assertions rather than in a separate test suite.

This is the second of three tiers in the Accessibility testing strategy: static-analysis linting catches structural violations in source, component tests catch the rendered-only ones (computed contrast, focus order, keyboard operability), and manual audits cover the subjective remainder.

Accessibility component test - keyboard navigation and WCAG assertions
// accessibility scanner setup

describe("Checkout flow", () => {
  it("should be completable using only the keyboard", async () => {
    render(<CheckoutPage />);

    await userEvent.tab();
    expect(screen.getByLabelText("Card number")).toHaveFocus();

    await userEvent.type(screen.getByLabelText("Card number"), "4111111111111111");
    await userEvent.tab();
    await userEvent.type(screen.getByLabelText("Expiry"), "12/27");
    await userEvent.tab();
    await userEvent.keyboard("{Enter}");

    expect(await screen.findByText("Order confirmed")).toBeInTheDocument();

    const results = await accessibilityScanner(document.body);
    expect(results).toHaveNoViolations();
  });
});

Connection to CD Pipeline

Component tests run after unit tests in the pipeline, but before longer running acceptance tests, and provide the broadest fast, deterministic feedback:

  1. Local development: run before committing. Deterministic scope keeps them fast enough to run locally without slowing the development loop.
  2. PR verification: CI executes the full suite; failures block merge.
  3. Trunk verification: the same tests run on the merged HEAD to catch conflicts.

They should always halt the CD pipeline on failure.

2 - Contract Tests

Deterministic tests that verify interface boundaries with external systems using test doubles. Also called narrow integration tests. Validated by integration tests running against real systems.
Consumer-driven contract flow: consumer team runs a component test against a provider test double, generating a contract artifact. The provider team runs a verification step against the real service using the consumer contract. Both sides discover different things: consumers check for fields and types they depend on; providers check they have not broken any consumer.

Definition

Verification that two separate systems (such as an API provider and its consumers, or a message publisher and subscriber) adhere to a shared, agreed-upon formal specification, the “contract”, without requiring both services to run simultaneously in an integrated environment.

Scope & Boundaries

Targets only the boundary interface: request payloads, query parameters, HTTP headers, response schemas, status codes, or message formats. It does not test internal business logic, database state, or deep end-to-end user journeys; it solely verifies compatibility with the interface schema (e.g., OpenAPI/Swagger, AsyncAPI, Protobuf).

Characteristics

Fast, independent execution across pipelines, prevents breaking schema changes before deployment, and eliminates the need for expensive, flaky end-to-end integration environments in applications with proper domain separation.

Good Practices:

  • Strict schema adherence: Explicitly define nullability, enums, required fields, and format constraints rather than relying on loose, open schemas.
  • Automated breaking-change detection: Integrate tools like oasdiff or buf breaking into CI to catch backwards-incompatible schema changes on pull requests.
  • Generate stubs directly from contracts: Use contract-driven mock engines (e.g., Prism, Microcks) so mock behavior automatically updates whenever the contract changes.

Anti-Patterns:

  • Testing business logic via contracts: Attempting to verify authorization rules, complex workflows, or algorithmic computations inside a contract test.
    • Example: testing that a request for a user return a specific user instead of the expected user object.
  • Hand-crafted, unverified mock fixtures: Manually updating JSON response stubs in consumer code repos without validating them against the live contract artifact.
  • All-or-nothing megaspecs: Coupling unrelated domains or multiple service interfaces into a single monolithic contract document that cannot be versioned or evolved independently.

Validating the Contract

A contract test only proves your code matches the contract - not that the contract still matches the real system. Integration tests close that gap by running against the real dependency. How tightly that loop closes depends on collaboration level: low collaboration means scheduled integration tests against the real system with no shared tooling; high collaboration adds a hosted specification server (Pact, Pacto), a specification fetched from a shared or provider repository at build time, or a provider webhook that triggers the consumer’s CI when the contract changes.

Consumer and Provider Perspectives

A contract has two sides asking different questions. The consumer asks whether the fields, types, and status codes it depends on still exist. Consumer tests assert only on the subset of the API the consumer actually uses, not the whole response - following Postel’s Law, be liberal in what you accept and conservative in what you send.

The provider asks whether its changes will break any consumer. A provider test runs every published consumer expectation against the real implementation, catching a removed field, a changed type, or altered error behavior before a consumer deploys and discovers the break.

Contract-First Development

The interface is defined as a formal artifact - an OpenAPI, Protobuf, or AsyncAPI spec - before either side writes an implementation. Consumer and provider teams build independently against that artifact, then verify conformance to the spec rather than to each other’s code. Works best for new APIs and parallel development, where there’s no existing implementation to write consumer-driven contracts against.

Examples

A consumer contract test using a consumer-driven contract tool:

Consumer contract test - order service consuming inventory API
describe("Order Service - Inventory Provider Contract", () => {
  it("should receive stock availability in the expected format", async () => {
    // Define what the consumer expects from the provider
    await contractTool.addInteraction({
      state: "item-42 is in stock",
      uponReceiving: "a request for item-42 stock",
      withRequest: { method: "GET", path: "/stock/item-42" },
      willRespondWith: {
        status: 200,
        body: {
          // Only assert on fields the consumer actually uses
          available: matchType(true),   // boolean
          quantity: matchType(10),      // integer
        },
      },
    });

    // Exercise the consumer code against the mock provider
    const result = await inventoryClient.checkStock("item-42");
    expect(result.available).toBe(true);
  });
});

A provider verification test that runs consumer expectations against the real implementation:

Provider verification - running consumer contracts against the real API
describe("Inventory Service - Provider Verification", () => {
  it("should satisfy all registered consumer contracts", async () => {
    await contractBroker.verifyProvider({
      provider: "InventoryService",
      providerBaseUrl: "http://localhost:3001",
      brokerUrl: "https://contract-broker.internal",
      providerVersion: process.env.GIT_SHA,
    });
  });
});

A contract-first schema validation test verifying a provider response against an OpenAPI spec:

Contract-first test - OpenAPI schema validation
// The OpenAPI document is the source of truth. Validate the whole response
// against the named schema rather than hand-checking individual fields - a
// field-by-field check drifts from the spec the moment the spec changes.
const validator = openApiValidator(openApiSpec);

describe("GET /stock/:id - OpenAPI contract", () => {
  it("should return a response conforming to the published schema", async () => {
    const response = await fetch("http://localhost:3001/stock/item-42");
    const body = await response.json();

    expect(response.status).toBe(200);

    // Asserts structure, types, required fields, and additionalProperties
    // rules exactly as the OpenAPI schema declares them.
    const result = validator.validate(body, "StockResponse");
    expect(result.errors).toEqual([]);
  });
});

Connection to CD Pipeline

Contract tests run after unit tests in the pipeline:

  1. Local development: run before committing. Deterministic scope keeps them fast enough to run locally without slowing the development loop.
  2. PR verification: CI executes the full suite; failures block merge.
  3. Trunk verification: the same tests run on the merged HEAD to catch conflicts.

They should always halt the CD pipeline on failure.

3 - End-to-End Tests

Tests that exercise two or more real components up to the full system. Non-deterministic by nature; never a pre-merge gate.
End-to-end test scope spectrum. Narrow scope: a test drives a real service that calls a real database. Full-system scope: a browser drives a real frontend, which calls a real backend, which calls a real database. All components are real at every scope - no test doubles.

Definition

Verification of a complete end-to-end user or business transaction through the entire deployed application stack, matching the perspective and experience of an actual user or external consumer.

Scope & Boundaries

Encompasses the entire system topology—from the frontend UI or external API gateway through all internal microservices, asynchronous workers, live queues, databases, and necessary third-party sandbox integrations.

Core Characteristics

Highest real-world confidence, highest execution cost, slowest run time, and highest vulnerability to environment or network-induced flakiness.

Good Practices

  • Restrict to critical revenue/operational paths: Focus E2E coverage strictly on non-negotiable user journeys (e.g., user registration, primary checkout, key ingest pipelines).
  • Automate environment provisioning: Deploy ephemeral, on-demand preview environments to run E2E suites and tear them down immediately upon completion.
  • Implement resilient element selection: Select UI elements using accessibility roles or stable data attributes (e.g., data-testid) rather than fragile CSS classes or absolute XPath selectors.

Anti-Patterns

  • Using E2E tests for regression safety nets: Relying on E2E suites to catch regressions that could have been detected upstream in unit, component, or contract stages (the “inverted testing pyramid”).
  • Arbitrary thread sleeps: Adding fixed pauses (e.g., sleep(5)) to wait for asynchronous events rather than using explicit, condition-driven polling.
  • Accepting flaky tests: Rerunning failing E2E tests until they turn green rather than quarantining and fixing the underlying timing or state issues immediately.

Weaknesses & Challenges

  • High Flakiness and Low Signal-to-Noise Ratio: Non-deterministic failures are common. Network blips, browser rendering lag, race conditions in asynchronous frontend frameworks, and transient third-party service outages often cause false-negative test failures that erode developer trust.
  • Poor Root-Cause Localization: When an E2E test fails with a generic error (e.g., TimeoutError: Element #confirmation-banner not found), finding the source of the issue requires combing through client logs, gateway routes, backend microservice traces, and database state to determine what actually broke.
  • Environment Maintenance & Resource Cost: E2E suites typically demand fully integrated staging or preview environments. Keeping these environments populated with realistic test data, configured with active credentials, and synchronized across dozens of microservices is notoriously resource-intensive.
  • Prohibitive Execution Times: Running full browser automation or multi-service distributed flows can take anywhere from tens of minutes to several hours. This latency breaks continuous delivery flow, encouraging teams to defer testing to late-stage, batch-processed pipelines rather than getting instant feedback on change.
  • Tight Coupling to Volatile UI/API Layouts: Small cosmetic changes (like modifying class names, reordering markup, or tweaking a multi-step user flow) often break brittle E2E tests even though the underlying business capability remains completely functional.

Examples

Example (Playwright UI / Full System Flow)
import { test, expect } from '@playwright/test';

test('user can complete entire purchase flow', async ({ page }) => {
  // Navigates real UI against a fully deployed environment
  await page.goto('https://checkout.staging.example.com');
  await page.fill('#username', 'test_user');
  await page.fill('#password', 'SecurePass123!');
  await page.click('button[type="submit"]');

  // Add item to cart and initiate purchase
  await page.click('button[data-item="product-42"]');
  await page.click('#cart-checkout');
  await page.fill('#card-element', '4242424242424242');
  await page.click('#submit-payment');

  // Confirms response propagated across API, async billing, and UI rendering
  await expect(page.locator('.order-confirmation')).toHaveText(/Order #\d+ Confirmed/);
});

When to Use / Avoid

Use them for:

  • Happy-path validation of critical business flows that cannot be verified any other way (e.g., a payment flow that depends on a real payment provider).
  • Entangled domain workflows that span multiple deployables and cannot be isolated within a single component test.

They are the most expensive test type to write, run, and maintain. Use them sparingly.

Avoid for:

  • Edge cases, error handling, or input validation. Those scenarios belong in unit or component tests.

Connection to CD Pipeline

E2E tests should only run in the pipeline as part of the longer running acceptance tests if they can be made dependable and deterministic. Otherwise, they should be run on a schedule and not act as a delivery decision.

4 - Integration Tests

Tests that exercise real external dependencies to validate that contract test doubles still match reality. Non-deterministic; never a pre-merge gate.

Definition

Verification that two or more distinct architectural subsystems or external dependencies interact correctly across their transport layer and data boundaries.

Scope & Boundaries

Broader than a component test because it explicitly validates communication with real external systems (such as a database, message queue, cache, or filesystem), but narrower than a full end-to-end test because it targets a specific integration boundary rather than complete multi-service user workflows.

Core Characteristics

Detects driver/dialect mismatches, schema serialization issues, connection pooling misconfigurations, and ORM/SQL query errors that mock-heavy tests overlook.

Good Practices

  • Use disposable, ephemeral infrastructure: Spin up real databases and queues using container tooling (e.g., Testcontainers) rather than using shared, persistent static environments.
  • Verify transport-level error handling: Intentionally test connection timeouts, pool exhaustion, network blips, and transaction rollbacks.
  • Run tests against clean boundary state: Truncate tables, flush caches, and clear queues between test runs to guarantee deterministic execution.

Anti-Patterns

  • Using shared remote environments: Pointing integration test suites to shared dev/staging databases, causing data collisions and race conditions between concurrent CI jobs.
  • Testing business permutations: Testing dozens of conditional logic branches through real databases instead of pushing that logic down to fast unit tests or leveraging component tests.
  • Ignoring production parity: Testing against an SQLite in-memory database locally when production runs Postgres, masking dialect, constraint, and indexing differences.

Weaknesses & Challenges

  • Infrastructure Orchestration Overhead: Requires managing real databases, message brokers, and caches inside the test execution context. Maintaining container definitions (e.g., Docker/Testcontainers) and keeping schema migrations up to date adds operational burden to developers.
  • Test Isolation and State Contamination: When tests write real rows to a database or publish messages to an active broker, dirty state from one test can bleed into another. Cleaning, truncating, or rolling back transactions between runs adds latency and complexity.
  • Slow Pipeline Feedback Cycles: Because integration tests involve real I/O, network socket handshakes, and disk writes, they are orders of magnitude slower than in-memory unit tests. Over-relying on them severely bloats commit-stage feedback loops.
  • Local vs. Production Discrepancies: Test-specific database configurations, lightweight containerized replicas, or local mocks often mask subtle production issues—such as database clustering behavior, regional latency, connection pool limits, or privilege boundaries.

Examples

Python Example (Repository-to-Database Integration):
import pytest
from sqlalchemy import create_engine
from myapp.storage import OrderRepository, Order

# Runs against an actual ephemeral Postgres container, not an in-memory mock
def test_order_repository_persists_and_retrieves_roundtrip(real_pg_session):
    repo = OrderRepository(session=real_pg_session)
    new_order = Order(order_id=101, customer_id="cust_abc", total=49.99)

    repo.save(new_order)
    retrieved = repo.find_by_id(101)

    assert retrieved is not None
    assert retrieved.customer_id == "cust_abc"
    assert retrieved.total == 49.99

Connection to CD Pipeline

Integration tests should only run in the pipeline as part of the longer running acceptance tests if they can be made dependable and deterministic. Otherwise, they should be run on a schedule and not act as a delivery decision.

5 - Static Analysis

Code analysis tools that evaluate non-running code for security vulnerabilities, complexity, and best practice violations.

Definition

Static analysis (also called static testing) evaluates non-running code against rules for known good practices, inspecting source, configuration, and dependency manifests to catch errors, complexity, and security issues before the code ever runs.

Scope & Boundaries

Analysis runs against source code, configuration files, and dependency manifests at rest - no application starts and no test doubles are needed. Scope is the entire codebase, not a single unit, component, or transaction.

Characteristics

Seconds-scale execution (the fastest test category), fully deterministic, and codebase-wide scope, with no external dependencies except the network calls a dependency scanner makes to a vulnerability database.

Good Practices

  • Run it everywhere feedback is possible: IDE plugins, pre-commit hooks, and CI each catch issues before the next stage makes them more expensive to fix.
  • Customize the ruleset: default rules are a starting point; add rules for patterns that keep coming up in code review.
  • Enforce it as a gate: treat lint, type, and security findings as build-breaking, the same as a failing test.

Anti-Patterns

  • Disabling rules instead of fixing code: suppressing linter warnings or ignoring security findings erodes the value of static analysis over time.
  • Skipping ruleset customization: default rules are a starting point, not a ceiling, for patterns specific to the codebase.
  • Running static analysis only in CI: by the time CI reports a formatting error, the developer has context-switched; IDE and pre-commit feedback catch it sooner.
  • Ignoring dependency vulnerabilities: known CVEs in dependencies are a direct attack vector and should break the build.
  • Treating static analysis as optional: if developers can bypass the checks, they will.

When to Run It

  • In the IDE: real-time feedback as developers type, via editor plugins and language server integrations.
  • On save: format-on-save and lint-on-save catch issues immediately.
  • Pre-commit: hooks prevent problematic code from entering version control.
  • In CI: the full suite of static checks runs on every PR and on the trunk after merge, verifying that earlier local checks were not bypassed.

Static analysis is always applicable. Every project, regardless of language or platform, benefits from linting, formatting, and dependency scanning.

Examples

Linting

A .eslintrc.json configuration enforcing test quality rules:

Linter configuration for test quality rules
{
  "rules": {
    "no-disabled-tests": "warn",
    "require-assertions": "error",
    "no-commented-out-tests": "error",
    "valid-assertions": "error",
    "no-unused-vars": "error",
    "no-console": "warn"
  }
}

Type Checking

Statically typed languages catch type mismatches at compile time, eliminating entire classes of runtime errors. Java, for example, rejects incompatible argument types before the code runs:

Java type checking example
public static double calculateTotal(double price, int quantity) {
    return price * quantity;
}

// Compiler error: incompatible types: String cannot be converted to double
calculateTotal("19.99", 3);

Dependency Scanning

Dependency scanning tools scan for known vulnerabilities:

npm audit output example
$ npm audit
found 2 vulnerabilities (1 moderate, 1 high)
  moderate: Prototype Pollution in lodash < 4.17.21
  high:     Remote Code Execution in log4j < 2.17.1

Types of Static Analysis

TypePurpose
LintingCatches common errors and enforces good practices
FormattingEnforces consistent code style, removing subjective debates
Complexity analysisFlags overly deep or long code blocks that breed defects
Type checkingPrevents type-related bugs, replacing some unit tests
Security scanningDetects known vulnerabilities and dangerous coding patterns
Dependency scanningChecks for outdated, hijacked, or insecurely licensed deps
Accessibility lintingDetects missing alt text, ARIA violations, contrast failures, semantic HTML issues

Accessibility Linting

Accessibility linting catches deterministic WCAG violations the same way a security scanner catches known vulnerability patterns. Automated checks cover structural issues (missing alt text, invalid ARIA attributes, insufficient contrast ratios, broken heading hierarchy) while manual review covers subjective aspects like whether alt text is actually meaningful.

Linting is the first of three tiers. For how it fits with component-test DOM scans and manual audits across the pipeline - and the caveat that automated checks catch only a fraction of WCAG criteria - see Accessibility testing.

An accessibility checker configuration running WCAG 2.1 AA checks against rendered pages:

Accessibility checker configuration for WCAG 2.1 AA
{
  "defaults": {
    "standard": "WCAG2AA",
    "timeout": 10000,
    "wait": 1000
  },
  "urls": [
    "http://localhost:1313/docs/",
    "http://localhost:1313/docs/foundations/testing-fundamentals/test-architecture/"
  ]
}

An accessibility scanner test asserting that a rendered component has no violations:

Accessibility scanner test verifying no WCAG violations
// accessibility scanner setup (e.g. import scanner and extend assertions)

it("should have no accessibility violations", async () => {
  const { container } = render(<LoginForm />);
  const results = await accessibilityScanner(container);
  expect(results).toHaveNoViolations();
});

Connection to CD Pipeline

Static analysis is the first gate in the CD pipeline, providing the fastest feedback:

  1. IDE / local development: plugins run in real time as code is written.
  2. Pre-commit: hooks run linters, formatters, and accessibility checks on changed components, blocking commits that violate rules.
  3. PR verification: CI runs the full static analysis suite (linting, type checking, security scanning, dependency auditing, accessibility linting) and blocks merge on failure.
  4. Trunk verification: the same checks re-run on the merged HEAD to catch anything missed.
  5. Scheduled scans: dependency and security scanners run on a schedule to catch newly disclosed vulnerabilities in existing dependencies.

Because it requires no running code and no external dependencies, static analysis is the cheapest, fastest gate. A mature CD pipeline treats its failures like any other test failure: they break the build.

6 - Smoke Tests

A lightweight, automated verification run executed immediately after code is deployed

Definition

A lightweight, automated verification run executed immediately after code is deployed to a live target environment (staging, pre-production, or production) to verify that the environment is healthy, operational, and capable of handling traffic before fully shifting user load.

Scope & Boundaries

Strictly limited to high-level system vitality. It checks that critical infrastructure components (processes, routing, database connectivity, secret access, core endpoints) are alive and reachable. It explicitly avoids deep workflow testing, exhaustive edge-case permutations, or long-running operational flows.

Core Characteristics

Fast (seconds to 2–3 minutes max), strictly non-destructive/read-only, high criticality, and directly tied to deployment orchestration (triggers immediate automated rollback or stops traffic migration if it fails).

Examples

Python API Smoke Test Script
import requests
import sys

BASE_URL = "https://checkout.internal.net"

def run_smoke():
    # 1. Deep health check (probes DB connection, cache, and queue availability)
    health = requests.get(f"{BASE_URL}/healthz/ready", timeout=5)
    assert health.status_code == 200, f"Health check failed: {health.text}"
    assert health.json().get("database") == "UP"

    # 2. Key static config / version check
    version = requests.get(f"{BASE_URL}/version", timeout=3)
    assert version.status_code == 200
    assert version.json().get("commit_sha") == sys.argv[1]

    # 3. Non-destructive read query against a core endpoint
    catalog = requests.get(f"{BASE_URL}/api/v1/products/featured", timeout=5)
    assert catalog.status_code == 200
    assert len(catalog.json()) > 0

if __name__ == "__main__":
    run_smoke()

A Java sociable unit test exercising real domain logic through its public interface. The collaborators (the pricing policy and the order model) are real objects, not mocks, and the test asserts on the observable outcome - the computed total - rather than on which methods were called:

Java sociable unit test for a bulk-discount pricing rule
@Test
public void appliesBulkDiscountWhenQuantityReachesThreshold() {
    // Arrange: real collaborators, no test doubles - this is pure in-process logic
    PricingPolicy pricing = new PricingPolicy(
        bulkThreshold(10), bulkDiscountRate(0.15));
    Order order = new Order(new LineItem("widget", money("20.00"), quantity(12)));

    // Act
    Money total = pricing.totalFor(order);

    // Assert: the observable result, not the sequence of internal calls
    // 12 * 20.00 = 240.00, less 15% = 204.00
    assertEquals(money("204.00"), total);
}

@Test
public void chargesFullPriceBelowTheThreshold() {
    PricingPolicy pricing = new PricingPolicy(
        bulkThreshold(10), bulkDiscountRate(0.15));
    Order order = new Order(new LineItem("widget", money("20.00"), quantity(9)));

    assertEquals(money("180.00"), pricing.totalFor(order));
}

Good Practices

  • Design for idempotency and read-only behavior: Keep smoke tests non-destructive so they can run safely against live production environments without corrupting customer data, charging credit cards, or sending false operational emails.
  • Target deep health endpoints: Use application endpoints that actively verify connectivity to backend resources (database reads, Redis caches, downstream dependency reachability) rather than shallow /ping endpoints that only return 200 OK from the web server process.
  • Automate immediate rollback gates: Tie smoke test outcomes directly into your CD pipeline (e.g., progressive delivery, canary, or blue/green deployments). If the smoke suite fails, the deployment halts and rolls back automatically without human intervention.
  • Verify deployment identity: Assert that the deployed system is serving the exact build artifact, tag, or Git commit hash intended for the deployment to catch caching or orchestration misconfigurations. Anti-Patterns
  • Mutating real production data: Creating synthetic test users, modifying real records, or generating phantom financial transactions without rigorous isolation or synthetic-data isolation strategies.
  • Bloating into a full regression suite: Packing dozens of detailed integration checks into the smoke run, slowing pipeline execution from seconds into tens of minutes and defeating the purpose of a fast sanity gate.
  • Ignoring downstream read timeouts: Setting long or infinite request timeouts that cause the deployment pipeline to hang indefinitely when an infrastructure route or firewall rule is misconfigured.
  • Running exclusively in staging: Skipping smoke tests in production under the false assumption that passing staging guarantees environment-specific configs, network policies, and IAM roles are correctly wired in production. Weaknesses & Challenges
  • Coarse-Grained Blind Spots: Smoke tests only verify that the front door is open and core pipes are connected; they cannot catch subtle business regressions, boundary calculation errors, or minor UI rendering glitches.
  • Risk of Production State Side Effects: If write checks are included, synthetic data can leak into operational reports, analytics dashboards, or customer views, requiring custom clean-up routines that are prone to failure.
  • Permissions and Security Boundaries: Probing internal services and operational health endpoints in locked-down production environments often requires elevated network routes or secure service tokens that must be strictly audited and maintained.
  • Handling Transient Startup Latency: Newly launched containers, warm-up caches, or JIT compilation can cause false-positive smoke failures immediately following a rollout if proper readiness probes and retry loops are not configured.

Connection to CD Pipeline

Smoke tests are run after every deploy to validate the deploy. They are also used to trigger auto-rollback in the pipeline if they don’t pass.

7 - Unit Tests

Fast, deterministic tests that verify a unit of behavior through its public interface, asserting on what the code does rather than how it works.
Solitary unit test: test actor sends input to a Unit Under Test; all collaborators are replaced by test doubles. Sociable unit test: test actor sends input to a Unit Under Test that uses real in-process collaborators; only external I/O is replaced by a test double.

Definition

Verification of the smallest testable piece of code—typically a single function, method, or class—in complete isolation from the rest of the application, network, file system, or external services. Test doubles are used where needed.

Scope & Boundaries

Execution runs entirely in-memory. All external dependencies (databases, APIs, message brokers, system clocks) are replaced with test doubles (stubs, mocks, or fakes).

Characteristics

Millisecond execution speeds, highly deterministic (zero flakiness), and pinpoint failure localization.

Good Practices:

  • Test public behavior, not implementation details: Assert on return values and visible side effects rather than internal private state or execution paths.
  • Strict isolation: Keep all tests in-memory; mock or stub out network, disk I/O, database, and system time to ensure sub-millisecond execution.
  • Single assertion concept: Each test should verify one specific behavior or edge case to maintain pinpoint failure localization.

Anti-Patterns

  • Over-mocking: Mocking domain entities, data transfer objects, or language primitives instead of purely external/infrastructure boundaries.
  • Testing private methods: Forcing visibility or coupling tests to internal helper methods, which causes refactoring resistance without increasing behavioral confidence.
  • Inter-test dependencies: Letting execution order matter or sharing mutable global state between test cases.

Solitary vs. sociable unit tests

A solitary unit test replaces all collaborators with test doubles. A sociable unit test allows real in-process collaborators while still replacing any external I/O. Both styles are unit tests as long as no real external dependency is involved.

When to Run Them

  • During development: run the relevant subset of unit tests continuously while writing code. TDD (Red-Green-Refactor) is the most effective workflow.
  • On every commit: use pre-commit hooks or watch-mode test runners so broken tests never reach the remote repository.
  • In CI: execute the full unit test suite on every pull request and on the trunk after merge to verify nothing was missed locally.

Unit tests are the right choice when the behavior under test can be exercised without network access, file system access, or database connections. If you need any of those, you likely need a component test or an end-to-end test instead.

Examples

JavaScript unit test for castArray utility
// castArray.test.js
describe("castArray", () => {
  it("should wrap non-array items in an array", () => {
    expect(castArray(1)).toEqual([1]);
    expect(castArray("a")).toEqual(["a"]);
    expect(castArray({ a: 1 })).toEqual([{ a: 1 }]);
  });

  it("should return array values by reference", () => {
    const array = [1];
    expect(castArray(array)).toBe(array);
  });

  it("should return an empty array when no arguments are given", () => {
    expect(castArray()).toEqual([]);
  });
});

A Java sociable unit test exercising real domain logic through its public interface. The collaborators (the pricing policy and the order model) are real objects, not mocks, and the test asserts on the observable outcome - the computed total - rather than on which methods were called:

Java sociable unit test for a bulk-discount pricing rule
@Test
public void appliesBulkDiscountWhenQuantityReachesThreshold() {
    // Arrange: real collaborators, no test doubles - this is pure in-process logic
    PricingPolicy pricing = new PricingPolicy(
        bulkThreshold(10), bulkDiscountRate(0.15));
    Order order = new Order(new LineItem("widget", money("20.00"), quantity(12)));

    // Act
    Money total = pricing.totalFor(order);

    // Assert: the observable result, not the sequence of internal calls
    // 12 * 20.00 = 240.00, less 15% = 204.00
    assertEquals(money("204.00"), total);
}

@Test
public void chargesFullPriceBelowTheThreshold() {
    PricingPolicy pricing = new PricingPolicy(
        bulkThreshold(10), bulkDiscountRate(0.15));
    Order order = new Order(new LineItem("widget", money("20.00"), quantity(9)));

    assertEquals(money("180.00"), pricing.totalFor(order));
}

Connection to CD Pipeline

Unit tests run in the earliest stages of the CD pipeline and provide the fastest feedback loop:

  1. Local development: watch mode reruns tests on every save.
  2. Pre-commit: hooks run the suite before code reaches version control.
  3. PR verification: CI runs the full suite and blocks merge on failure.
  4. Integrated change verification: CI reruns tests on the merged HEAD to catch integration issues.

They should always halt the CD pipeline on failure.