liveBook · Manning

Dependency

As explained in The Art of Unit Testing 3e

An imported or required software component on which a module relies.

A dependency is something a unit of work relies on but does not fully control during a unit test—or something that would be miserable to control. A logger, database, filesystem, network, clock, asynchronous work, or code owned by another team can all qualify. A fast, in-memory collaborator that is easy to configure usually does not. The practical question is not whether code was imported, but whether it introduces behavior the test cannot reliably direct. section 1.3 section 3.0

Dependencies matter because they create paths out of a unit of work and paths back into it. Calling a logger, saving to a database, sending email, or notifying an API is an outgoing dependency: the call is an observable exit point. Receiving a query result, file contents, or network response is an incoming dependency: it supplies indirect input for the logic under test. section 3.1 section 1.3

Figure 3.1 On the left, an exit point is implemented as invoking a dependency. On the right, the dependency provides indirect input or behavior and is not an exit point.
03-01

Compare outgoing calls that end a logical flow with incoming data or behavior supplied to the unit of work. section 3.1

Tests normally break dependency links with test doubles, choosing the double by the dependency's direction:

  • Use a stub for an incoming dependency. Configure a fake function, module, or object to return the data or behavior needed by the test; do not assert against the stub. section 3.1 section 3.8
  • Use a mock for an outgoing dependency. Replace it with a fake and assert that the expected call occurred, because that call is an exit point. section 3.1 section 3.8

Logger as an outgoing dependency

A password-verification function can return true or false and also call log.info("PASSED") or log.info("FAIL"). The logger call is a separate exit point from the return value. Depending directly on a complicated imported logger makes ordinary injection difficult; introducing a seam lets a test supply a fake logger and check the message without writing to a real destination. section 4.2 section 1.3

A seam is a place where pieces of code meet and where a fake can be injected. Common options are adding a parameter, using currying or higher-order functions, abstracting a module, or injecting an object or interface. Modular injection can retain original dependencies, allow temporary replacements, and reset them afterward. It adds production-code structure, but makes replacement easier when a module-style design is required. section 4.2 section 4.5.2

Direct dependencies are convenient, but can tie tests to a third-party API. If that API changes, many tests may need to change too. A small internal adapter can hold the direct reference and expose a simpler API the application controls. Framework mocking can help with baked-in or legacy dependencies, but it can also hide an opportunity to create that abstraction. section 5.2.2

Controlling dependencies makes low-level tests deterministic: initial state and collaborator responses are predetermined, so failures point more directly to changed logic. Real databases, networks, configuration, and services add moving parts. They can increase confidence at higher test levels, but also increase flakiness, setup effort, and debugging cost. When a test is flaky, fix it by controlling its dependencies, convert it to a lower-level test with fakes, or decide whether to remove it. section 7.5 section 7.5.1

Keep the test level separate from its abstraction level. A component test may cover several functions or components and still be a unit-level test when everything remains in memory and under test control. Such tests tend to be fast, maintainable, and less flaky; tests with real external dependencies belong closer to integration testing. section 10.1.2 section 1.11

How The Art of Unit Testing 3e covers it

How deep it goes
Defines dependency types, test doubles, injection seams, maintainability trade-offs, and flaky-test responses.
Examples
Uses a logger in password verification, database and network calls, file contents, clocks, and UI component tests.
What it assumes
Introduces dependency control from basic function-sized units of work without requiring prior framework knowledge.
What you can do after
Diagnose/debug with dependencies by classifying their direction, selecting stubs or mocks, and reducing flaky moving parts.
How it connects
Connects dependencies with unit testing, integration testing, mocks, stubs, seams, adapters, and flaky tests.

Also explained in

Compare how 27 books explain dependency →

Related concepts

This explainer was generated by Manning's knowledge graph from the text of The Art of Unit Testing 3e. The section links point to the parts of the book it draws on.