For AI agents: a documentation index is available at /llms.txt.
This page has a Markdown version at /docs/foundations/testing-fundamentals/test-types/index.html.md.
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.
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 responsehttpMock("https://inventory.internal").onGet("/stock/item-42").reply(200,{available:true,quantity:10});// Act: send a request through the full application stackconst response =awaitrequest(app).post("/orders").send({itemId:"item-42",quantity:2});// Assert: verify the public interface responseexpect(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 =awaitrequest(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 setupdescribe("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 =awaitaccessibilityScanner(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:
Local development: run before committing. Deterministic scope keeps them fast
enough to run locally without slowing the development loop.
PR verification: CI executes the full suite; failures block merge.
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.
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 providerawait 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 usesavailable:matchType(true),// booleanquantity:matchType(10),// integer},},});// Exercise the consumer code against the mock providerconst 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 =awaitfetch("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:
Local development: run before committing. Deterministic scope keeps them fast
enough to run locally without slowing the development loop.
PR verification: CI executes the full suite; failures block merge.
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.
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 environmentawait 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 purchaseawait 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 renderingawaitexpect(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:
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
publicstaticdoublecalculateTotal(double price,int quantity){return price * quantity;}// Compiler error: incompatible types: String cannot be converted to doublecalculateTotal("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
Flags overly deep or long code blocks that breed defects
Type checking
Prevents type-related bugs, replacing some unit tests
Security scanning
Detects known vulnerabilities and dangerous coding patterns
Dependency scanning
Checks for outdated, hijacked, or insecurely licensed deps
Accessibility linting
Detects 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
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 =awaitaccessibilityScanner(container);expect(results).toHaveNoViolations();});
Connection to CD Pipeline
Static analysis is the first gate in the CDpipeline, providing the fastest feedback:
IDE / local development: plugins run in real time as code is written.
Pre-commit: hooks run linters, formatters, and accessibility checks on changed
components, blocking commits that violate rules.
PR verification: CI runs the full static analysis suite (linting, type checking,
security scanning, dependency auditing, accessibility linting) and blocks merge on
failure.
Trunk verification: the same checks re-run on the merged HEAD to catch anything
missed.
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())>0if __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
@TestpublicvoidappliesBulkDiscountWhenQuantityReachesThreshold(){// Arrange: real collaborators, no test doubles - this is pure in-process logicPricingPolicy pricing =newPricingPolicy(bulkThreshold(10),bulkDiscountRate(0.15));Order order =newOrder(newLineItem("widget",money("20.00"),quantity(12)));// ActMoney total = pricing.totalFor(order);// Assert: the observable result, not the sequence of internal calls// 12 * 20.00 = 240.00, less 15% = 204.00assertEquals(money("204.00"), total);}@TestpublicvoidchargesFullPriceBelowTheThreshold(){PricingPolicy pricing =newPricingPolicy(bulkThreshold(10),bulkDiscountRate(0.15));Order order =newOrder(newLineItem("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.
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).
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.jsdescribe("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
@TestpublicvoidappliesBulkDiscountWhenQuantityReachesThreshold(){// Arrange: real collaborators, no test doubles - this is pure in-process logicPricingPolicy pricing =newPricingPolicy(bulkThreshold(10),bulkDiscountRate(0.15));Order order =newOrder(newLineItem("widget",money("20.00"),quantity(12)));// ActMoney total = pricing.totalFor(order);// Assert: the observable result, not the sequence of internal calls// 12 * 20.00 = 240.00, less 15% = 204.00assertEquals(money("204.00"), total);}@TestpublicvoidchargesFullPriceBelowTheThreshold(){PricingPolicy pricing =newPricingPolicy(bulkThreshold(10),bulkDiscountRate(0.15));Order order =newOrder(newLineItem("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:
Local development: watch mode reruns tests on every save.
Pre-commit: hooks run the suite before code reaches version control.
PR verification: CI runs the full suite and blocks merge on failure.
Integrated change verification: CI reruns tests on the merged HEAD to catch integration issues.
They should always halt the CD pipeline on failure.