Published June 1, 2022
by Doug Klugh

Find Bugs Easily

Promote testability of your software by isolating the System Under Test (SUT) to ensure that each test verifies only the intended condition.  This will foster Defect Localization and support Repeatable Tests, while minimizing overlap and dependencies between tests.  This will also impede attempts to verify complex states or multiple scenarios due to lack of control of all inputs to the SUT.  Clarify the control points and observation points of the SUT to facilitate effective interactions with the SUT.

Substituting Dependencies

Isolating that portion of the system that is to be tested ensures that we only have to focus on code paths through a single object.  Since unit tests verify Single Test Conditions by executing a single code path through the SUT, they rarely require additional work to isolate that portion of the code.  However, component, API, and integration tests often require the use of test doubles to act as substitutes for internal and external dependencies — those other components, objects or services the SUT depends upon.

Failing to isolate dependencies will foster fragile tests as there will be numerous states that can cause test failures.  And when they do fail, it will be difficult to determine which of those dependencies caused the failures.  It also becomes very difficult to test all the paths through the code, while observing the side effects — just because there are so many variables.  Those dependencies may return values or throw exceptions that affect the behavior of the SUT, making it very difficult to consistently execute certain test cases.  It may also be the case that certain resources are simply not available within a test environment.

There will likely be internal components that the SUT depends upon, but you will also likely have external components that provide indirect input to the SUT such as third-party libraries, databases, file systems, eMails, web services, system resources (i.e. clock), configurations, random(), etc.  All of these will likely need to be isolated from the SUT to limit the scope to a single test condition.

Polymorphic Dispatch

Building different implementations of an abstract class enables us to dynamically choose which type of object best fulfills our current needs and helps to realize specific use cases.  This further enables us to substitute test doubles for production resources, providing isolation of the SUT during testing.

Apply the Dependency Inversion Principle to create architectural boundaries (or seams) between those production resources and the SUT, making the calls to those resources polymorphic.  Through the use of object-oriented languages, polymorphism give us control over our dependency structure.  This will eliminate the coupling between the production resources and the SUT.

Through the use of dependency injection, test doubles can be substituted for these dependencies — providing full control over those indirect inputs to the SUT — including return values, exceptions, and side effects.  If nothing else, this promotes consistent execution of test cases and greatly enhances Defect Localization.

Figure 1 - Polymorphic Dispatch
Test Doubles

Test Doubles are objects that are used as substitutes for production resources during testing.  The following five types of test doubles serve as everything from simple placeholders to full-fledged simulators.  The purpose is to provide full control of those inputs to the SUT in order to stage the conditions being tested.

Dummies

A Dummy is the simplest form of a test double.  It implements an interface, while the functions do nothing.  If a function returns a value, it returns a null or a value as close to zero as possible.  Dummies are most often used as an argument to a function, where neither the test nor the function cares what happens to that argument.  A dummy serves as a placeholder where an object is required.

Stubs

A Stub is a dummy.  Its functions do nothing, except to return fixed values that are consistent with the needs of the tests.  Stub are used to direct execution of the code through certain pathways to be tested.

Spies

A Spy is a stub.  Its functions perform no external actions and returns values that drive execution of the code through certain pathways to be tested.  A spy remembers facts so the tests can verify that those functions where called properly.  Spies are used to determine whether a function was ever called, how many times it was called, how many arguments were passed in, what were those arguments, etc.  The only thing a spy does is watch and remember.

Mocks

A Mock is a spy.  Its functions do nothing, except to return values that are useful to the tests and remember facts about the way it was called.  A mock sets up conditions that are to be tested and evaluates whether those conditions have been met.  The test does not check what the mock spied on.  A mock knows what should happen and reports the success or failure back to the test.

Fakes

A Fake is different from other types of test doubles.  It is often used to simulate external devices or services, so it usually contains a lot of logic and can become complex.  The more the system grows, the more the fake grows — which can present maintenance challenges.  Because of their complexity, fakes should be kept to a minimum.

Hierarchy of Test Doubles