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
A component test exercises one component through its public interface: one backend service through its HTTP, gRPC, or GraphQL API, or one frontend component (or app shell) through its rendered DOM. The test treats that component as a black box: inputs go in through the public interface, observable outputs come out (response, persisted state, emitted event, rendered DOM, side effect), and the test asserts only on those outputs.
The component’s real internal modules are wired together - routing, validation, business logic, and persistence in a backend, or rendering, state management, and event handling in a UI. What gets replaced is whatever crosses the component’s boundary into a system the team doesn’t control: third-party APIs, downstream services owned by other teams, message brokers. Those become test doubles.
The component’s own persistence layer is the boundary that admits a choice. Two configurations are both valid component tests:
Doubled persistence: an in-memory repository or fake stands in for the database. Tests are fastest. Good for backend logic that doesn’t depend on SQL semantics.
Real production engine in a testcontainer: Postgres, MySQL, or whatever the production engine is, run in a per-test container or a transaction that rolls back at teardown. Slightly slower but exercises the real query plan, real constraints, real migration. The page on the API provider pattern covers when to prefer each.
A component test does not exercise more than one component end-to-end. A test that drives a UI which calls a real backend which writes to a real database is a fullstack flow - that’s an end-to-end test. Each component gets its own component tests at its own boundary; the frontend has its tests against a doubled backend, the backend has its tests against a doubled downstream and a real-or-doubled DB.
This is broader than a sociable unit test: a sociable unit test exercises a single behavior through a few collaborators; a component test exercises the entire assembled component through its public interface.
When component tests earn their keep
A component test overlaps with the combination of provider contract tests, sociable unit tests, and spies on collaborators. Each of those layers covers part of what a component test asserts. Component tests pull their weight when they catch something the other layers can’t, or when they let a single test answer a single user-story-level question.
They earn their keep when the component has:
Cross-cutting behavior at the seams. Auth, multi-tenancy, persistence, and event emission interacting on a single request is where production bugs live. Each layer in isolation may pass; the seam between them is what a component test exercises.
Non-trivial framework wiring. Middleware ordering, error-handler mapping (does a domain exception become 409 or 500?), DI-container configuration, request-body limits. Spy-based unit tests bypass all of this. Contract tests bypass it unless they exercise the fully booted app.
Acceptance criteria you want to map 1:1 to tests. A test that says “POST /orders with valid payment returns 201 and emits OrderPlaced” reads as the user story. The fragmented equivalent (contract test for shape + unit test for domain + spy for delegation + unit test for emission) covers the same ground but no single test reads as the story.
Realistic UI flows. Keyboard navigation, focus management, and screen-reader announcements need the rendered DOM, not a unit test of a component class.
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.
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.
Two boundary cases worth naming:
A test that needs to span more than one component (a real frontend driving a real backend) is an end-to-end test, not a component test.
A test that exercises a single unit of behavior through a few collaborators is a unit test, not a component test.
Characteristics
Property
Value
Speed
Milliseconds to seconds
Determinism
Always deterministic
Scope
One backend service or one frontend component
Dependencies
Systems the team doesn’t control are doubled
Network
Localhost only (testcontainers permitted)
Database
Doubled (in-memory) or production engine in a per-test testcontainer
Breaks build
Yes
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.
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();});});
Anti-Patterns
Calling a live external service the team doesn’t own: real network calls to a third-party API or another team’s service make the test non-deterministic and slow. Replace anything across the component boundary with a test double of a thin gateway you own.
Spanning more than one component: a test that drives a UI, makes a real network call to a backend, and waits for a real DB write is a fullstack flow, not a component test. Each component gets its own component tests at its own boundary; the cross-component flow belongs in end-to-end tests, and only for the few cases that can’t be covered any other way.
Sharing a live, mutable database between tests: leftover state and ordering dependencies produce flakes and “works on my machine” failures. The fix isn’t necessarily “no real DB”. A per-test testcontainer or a per-test transaction with rollback gives you the production engine and isolation. The anti-pattern is the shared, mutable part.
Ignoring the actor’s perspective: component tests should interact with the system
the way a user or API consumer would. Reaching into internal state or bypassing the
public interface defeats the purpose.
Duplicating unit test coverage: component tests should focus on feature-level
behavior and happy/critical paths. Leave exhaustive edge case and permutation testing
to unit tests.
Slow test setup: if bootstrapping the component takes too long, invest in faster
initialization (in-memory stores, lazy loading) rather than skipping component tests.
Deferring accessibility testing to manual audits: automated WCAG checks in
component tests catch violations on every commit. Quarterly audits find problems that
are weeks old.
Connection to CD Pipeline
Component tests run after unit tests in the pipeline and provide the broadest fast,
deterministic feedback before code is promoted:
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.
Pre-deployment gate: component tests can serve as the final deterministic gate
before a build artifact is promoted.
Because component tests are deterministic, they should always break the build on
failure. A healthy CD pipeline relies
on a strong component test suite to verify assembled behavior - not just individual
units - before any code reaches an environment with real dependencies.
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
A contract test (also called a narrow integration test) is a deterministic test that
validates your code’s interaction with an external system’s interface using
test doubles. It verifies that the boundary
layer code - HTTP clients, database query layers, message producers - correctly handles
the expected request/response shapes, field names, types, and status codes.
A contract test validates interface structure, not business behavior. It answers
“does my code correctly interact with the interface I expect?” not “is the logic correct?”
Business logic belongs in component tests.
Because contract tests use test doubles rather than live systems, they are
deterministic and run on every commit as part of the pipeline. They block the build
on failure, just like unit and component tests.
Integration tests validate that contract
test doubles still match the real external systems by running against live dependencies
post-deployment.
Consumer and Provider Perspectives
Every contract has two sides. The questions each side is trying to answer are different.
Consumer contract testing
The consumer is the service or component that depends on an external API. A consumer
contract test asks:
“Do the fields I depend on still exist, in the types I expect, with the status codes
I handle?”
Consumer tests assert only on the subset of the API the consumer actually uses - not
everything the provider exposes. A consumer that only needs id and email from a user
object should not assert on address or phone. This allows providers to add new fields
freely without breaking consumers.
Following Postel’s Law - “be conservative in what you send, be liberal in what you accept”
consumer tests should accept any valid response that contains the fields they need, and
tolerate fields they do not use.
What a consumer is trying to discover:
Has the provider changed or removed a field I depend on?
Has the provider changed a type I expect (string to integer, object to array)?
Has the provider changed a status code I handle?
Does the provider still accept the request format I send?
Provider contract testing
The provider is the service that owns the API. A provider contract test asks:
“Have my changes broken any of my consumers?”
A provider runs contract tests to verify that its API responses still satisfy the
expectations of every known consumer. This gives early warning - before any consumer
deploys and discovers the breakage - that a change is breaking.
What a provider is trying to discover:
Have I removed or renamed a field that a consumer depends on?
Have I changed a type in a way that breaks deserialization for a consumer?
Have I changed error behavior (status codes, error formats) that consumers handle?
Is my API still backward compatible with all published consumer expectations?
Approaches to Contract Testing
Consumer-driven contract development
In consumer-driven contracts (CDC), the consumer writes the contract. The consumer
defines their expectations as executable tests - what request they will send and what
response shape they require. These expectations are published to a shared contract broker and the provider runs them
as part of their own build.
The flow:
Consumer team writes tests defining their expectations against a mock provider.
The contract is published to a shared contract broker.
The provider team runs the consumer’s contract expectations against their real
implementation.
If the provider’s implementation satisfies the contract, the provider can deploy
with confidence it will not break this consumer. If not, the teams negotiate before
merging the breaking change.
CDC works well for evolving systems: it grounds the API design in actual consumer
needs rather than the provider’s assumptions about what consumers will use.
Contract-first development
In contract-first development, the interface is defined as a formal artifact -
an OpenAPI specification, a Protobuf schema, an Avro schema, or similar - before
any implementation is written. Both the consumer and provider code are generated from
or validated against that artifact.
The flow:
Teams agree on the interface contract (usually during design or story refinement).
The contract is committed to version control.
Consumer and provider teams develop independently, each generating or validating
their code against the contract.
Tests on both sides verify conformance to the contract - not to each other’s
implementation.
Contract-first works well for new APIs and parallel development: it lets consumer
and provider teams work simultaneously without waiting for a real implementation, and
makes the interface an explicit design decision rather than an emergent one.
Choosing between them
Situation
Prefer
Existing API with multiple consumers, evolving over time
Consumer-driven (CDC)
New API, teams working in parallel
Contract-first
Third-party API you do not control
Consumer-only contract tests (no provider side)
Public API with external consumers you cannot reach
Provider tests against published spec
The two approaches are not mutually exclusive. A team may define an initial contract-first
schema and then adopt CDC tooling as the number of consumers grows.
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
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();// Validate against the OpenAPI schema, not specific valuesexpect(response.status).toBe(200);expect(typeof body.available).toBe("boolean");expect(typeof body.quantity).toBe("number");// Additional fields the consumer does not use are not asserted on});});
Anti-Patterns
Asserting on business logic: contract tests verify structure, not behavior. A contract
test that asserts quantity > 0 when in stock is crossing into business logic territory.
That belongs in component tests.
Asserting on fields the consumer does not use: over-specified consumer contracts make
providers brittle. Only assert on what your code actually reads.
Testing specific data values: asserting that name equals "Alice" makes the test
brittle. Assert on types, required fields, and status codes instead.
Hitting live systems in contract tests: contract tests must use test doubles to stay
deterministic. Validating doubles against live systems is the role of
integration tests, which run post-deployment.
Running infrequently: contract tests should run often enough to catch drift before it
causes a production incident. High-volatility APIs may need hourly runs.
Skipping provider verification in CDC: publishing consumer expectations is only half
the pattern. The provider must actually run those expectations for CDC to work.
Connection to CD Pipeline
Contract tests run on every commit as part of the deterministic pipeline:
Contract tests in the pipeline
On every commit Unit tests Deterministic Blocks
Component tests Deterministic Blocks
Contract tests Deterministic Blocks
Post-deployment Integration tests Non-deterministic Validates contract doubles
E2E smoke tests Non-deterministic Triggers rollback
Contract tests verify that your boundary layer code correctly interacts with the
interfaces you depend on. Integration tests
validate that those test doubles still match the real external systems by running
against live dependencies post-deployment.
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
An end-to-end test exercises real components working together - no
test doubles replace the dependencies under
test. The scope ranges from two services calling each other,
to a service talking to a real database, to a complete user journey through every
layer of the system.
The defining characteristic is that real external dependencies are present: actual
databases, live downstream services, real message brokers, or third-party APIs.
Because those dependencies introduce timing, state, and availability factors outside
the test’s control, end-to-end tests are typically non-deterministic. They fail
for reasons unrelated to code correctness - network instability, service unavailability,
test data collisions, or third-party rate limits.
Terminology note
“Integration test” and “end-to-end test” are often used interchangeably in the
industry. Martin Fowler distinguishes between narrow integration tests (which use test
doubles at the boundary - what this site calls
contract tests) and broad integration tests
(which use real dependencies). This site treats them as distinct categories:
integration tests validate that contract
test doubles still match the real external systems, while end-to-end tests exercise
user journeys or multi-service flows through real systems.
Scope
End-to-end tests cover a spectrum based on how many components are real:
Scope
Example
Narrow
A service making real calls to a real database
Service-to-service
Order service calling the real inventory service
Multi-service
A user journey spanning three live services
Full system
A browser test through a staging environment with all dependencies live
All of these involve real external dependencies. All share the same fundamental
non-determinism risk. Use the narrowest scope that gives you the confidence you need.
When to Use
Use end-to-end tests sparingly. They are the most expensive test type to write,
run, and maintain. Use them for:
Smoke testing a deployed environment to verify that key integrations are
functioning after a deployment.
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).
Cross-team workflows that span multiple deployables and cannot be isolated
within a single component test.
Do not use end-to-end tests to cover edge cases, error handling, or input
validation. Those scenarios belong in unit or
component tests, which are faster, cheaper, and
deterministic.
Vertical vs. horizontal
Vertical end-to-end tests target features owned by a single team:
An order is created and the confirmation email is sent.
A user uploads a file and it appears in their document list.
Horizontal end-to-end tests span multiple teams:
A user navigates from homepage through search, product detail, cart, and checkout.
Horizontal tests have a large failure surface and are significantly more fragile.
They are not suitable for blocking the pipeline; run them on a schedule and
review failures out-of-band.
Characteristics
Property
Value
Speed
Seconds to minutes per test
Determinism
Typically non-deterministic
Scope
Two or more real components, up to the full system
Dependencies
Real services, databases, brokers, third-party APIs
Network
Full network access
Database
Live databases
Breaks build
No - triggers review or rollback, not a pre-merge gate
Examples
A narrow end-to-end test verifying a service against a real database:
Narrow E2E - order service against a real database
describe("OrderRepository (real database)",()=>{it("should persist and retrieve an order by ID",async()=>{const order =await orderRepository.create({itemId:"item-42",quantity:2,customerId:"cust-99",});const retrieved =await orderRepository.findById(order.id);expect(retrieved.itemId).toBe("item-42");expect(retrieved.status).toBe("pending");});});
A full-system browser test using a browser automation framework:
Full-system E2E - add to cart and checkout with browser automation
test("user can add an item to cart and check out",async({ page })=>{await page.goto("https://staging.example.com");await page.getByRole("link",{name:"Running Shoes"}).click();await page.getByRole("button",{name:"Add to Cart"}).click();await page.getByRole("link",{name:"Cart"}).click();awaitexpect(page.getByText("Running Shoes")).toBeVisible();await page.getByRole("button",{name:"Checkout"}).click();awaitexpect(page.getByText("Order confirmed")).toBeVisible();});
Anti-Patterns
Using end-to-end tests as the primary safety net: this is the ice cream cone
anti-pattern. The majority of your confidence should come from unit and
component tests, which are fast and
deterministic. End-to-end tests are expensive insurance for the gaps.
Blocking the pipeline: end-to-end tests must never be a pre-merge gate. Their
non-determinism will eventually block a deploy for reasons unrelated to code quality.
Blocking on horizontal tests: horizontal tests span too many teams and failure
surfaces. Run them on a schedule and review failures as a team.
Ignoring flaky failures: track frequency and root cause. A test that fails for
environmental reasons is not providing a code quality signal - fix it or remove it.
Testing edge cases here: exhaustive permutation testing in end-to-end tests is
slow, expensive, and duplicates what unit and component tests should cover.
Not capturing failure context: end-to-end failures are expensive to debug. Capture
screenshots, network logs, and video recordings automatically on failure.
Connection to CD Pipeline
End-to-end tests run after deployment, not before:
A team may choose to gate on a small, highly reliable set of vertical end-to-end
smoke tests immediately after deployment. This is acceptable only if the team invests
in keeping those tests stable. A flaky smoke gate is worse than no gate: it trains
developers to ignore failures.
Use contract tests to verify that the
test doubles in your component tests still
match reality. This gives you deterministic pre-merge confidence without depending on
live external systems.
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.
“Integration test” is widely used but inconsistently defined. On this site, integration
tests are tests that involve real external dependencies - actual databases, live
downstream services, real message brokers, or third-party APIs. They are non-deterministic
because those dependencies introduce timing, state, and availability factors outside the
test’s control.
Integration tests serve a specific role in the test architecture: they validate that the
test doubles used in your
contract tests still match reality. Without
integration tests, contract test doubles can silently drift from the real behavior of the
systems they simulate - giving false confidence.
Because integration tests depend on live systems, they run post-deployment or on a
schedule - never as a pre-merge gate. Failures trigger review or rollback decisions, not
build failures.
For tests that validate interface boundaries using test doubles (deterministic), see
Contract Tests.
For full-system browser tests and multi-service smoke tests, see
End-to-End Tests.
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. Unlike other test types that execute code and observe behavior, static
analysis inspects source code, configuration files, and dependency manifests to detect
problems before the code ever runs.
Static analysis serves several key purposes:
Catches errors that would otherwise surface at runtime.
Warns of excessive complexity that degrades the ability to change code safely.
Identifies security vulnerabilities and coding patterns that provide attack vectors.
Enforces coding standards by removing subjective style debates from code reviews.
Alerts to dependency issues such as outdated packages, known CVEs, license
incompatibilities, or supply-chain compromises.
When to Use
Static analysis should run continuously, at every stage where feedback is possible:
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.
Characteristics
Property
Value
Speed
Seconds (typically the fastest test category)
Determinism
Always deterministic
Scope
Entire codebase (source, config, dependencies)
Dependencies
None (analyzes code at rest)
Network
None (except dependency scanners)
Database
None
Breaks build
Yes
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.
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();});
Anti-Patterns
Disabling rules instead of fixing code: suppressing linter warnings or ignoring
security findings erodes the value of static analysis over time.
Not customizing rules: default rulesets are a starting point. Write custom rules for
patterns that come up repeatedly in code reviews.
Running static analysis only in CI: by the time CI reports a formatting error, the
developer has context-switched. IDE plugins and pre-commit hooks provide immediate feedback.
Ignoring dependency vulnerabilities: known CVEs in dependencies are a direct attack
vector. Treat high-severity findings as build-breaking.
Treating static analysis as optional: static checks should be mandatory and enforced.
If developers can bypass them, they will.
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 static analysis requires no running code, no test environment, and no external
dependencies, it is the cheapest and fastest form of quality verification. A mature CD
pipeline treats static analysis failures the same as test failures: they break the build.
6 - 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
A unit test is a deterministic test that exercises a unit of behavior (a single
meaningful action or decision your code makes) and verifies that the observable outcome is
correct. The “unit” is not a function, method, or class. It is a behavior: given these inputs,
the system produces this result. A single behavior may involve one function or several
collaborating objects. What matters is that the test treats the code as a
black box and asserts only on what it produces,
not on how it produces it.
A solitary unit test replaces all collaborators with test doubles and exercises a single
class or function in complete isolation.
A sociable unit test allows real in-process collaborators to participate - for example,
a service object calling a real domain model - while still replacing any external I/O (network,
database, file system) with test doubles. Both styles are unit tests as long as no real external
dependency is involved.
When the scope expands to an entire frontend component or a complete backend service exercised
through its public API, that is a component test.
White box testing (asserting on internal method
calls, call order, or private state) creates change-detector tests that break during routine
refactoring without catching real defects. Prefer testing through the public interface (methods,
APIs, exported functions) and asserting on return values, state changes visible to consumers,
or observable side effects.
The purpose of unit tests is to:
Verify that a unit of behavior produces the correct observable outcome.
Cover high-complexity logic where many input permutations exist, such as business rules, calculations, and state transitions.
Keep cyclomatic complexity visible and manageable through good separation of concerns.
When to Use
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.
Characteristics
Property
Value
Speed
Milliseconds per test
Determinism
Always deterministic
Scope
A single unit of behavior
Dependencies
All replaced with test doubles
Network
None
Database
None
Breaks build
Yes
Examples
A JavaScript unit test verifying a pure utility function:
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 unit test using a mocking framework to isolate the system under test:
Java unit test with mocking framework stub isolating the controller
White box testing: asserting on internal
state, call order, or private method behavior rather than observable output. These
change-detector tests break during refactoring without catching real defects. Test through
the public interface instead.
Testing private methods: private implementations are meant to change. They are
exercised indirectly through the behavior they support. Test the public interface instead.
No assertions: a test that runs code without asserting anything provides false
confidence. Lint rules can catch this automatically.
Disabling or skipping tests: skipped tests erode confidence over time. Fix or remove
them.
Confusing “unit” with “function”: a unit of behavior may span multiple collaborating
objects. Forcing one-test-per-function creates brittle tests that mirror the implementation
structure rather than verifying meaningful outcomes.
Ice cream cone testing: relying primarily on slow E2E tests while neglecting fast unit
tests inverts the test pyramid and slows feedback.
Chasing coverage numbers: gaming coverage metrics (e.g., running code paths without
meaningful assertions) creates a false sense of confidence. Focus on behavior coverage
instead.
Connection to CD Pipeline
Unit tests occupy the base of the test pyramid. They 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.
Trunk verification: CI reruns tests on the merged HEAD to catch integration issues.
Because unit tests are fast and deterministic, they should always break the build on failure.
A healthy CD pipeline depends on a large, reliable suite of
black box unit tests that verify behavior
rather than implementation, giving developers the confidence to refactor freely and ship
small changes frequently.