Sociable Tests run real code. That’s good for catching errors, but if the dependency chain includes infrastructure—external systems or state—they become hard to manage. The following patterns allow you to “turn off” external dependencies while retaining the benefits of sociable and state-based testing.
Nullables
Narrow Integration Tests are slow and difficult to set up. Although they’re useful for ensuring that low-level Infrastructure Wrappers work in practice, they’re overkill for code that depends on those wrappers. Therefore:
Program code that includes infrastructure in its dependency chain to have a createNull() factory method. The factory should create a “Nulled” instance that disables all external communication, but behaves normally in every other respect.
Make sure it supports Parameterless Instantiation.
Nullables were originally inspired by the Null Object pattern, but have evolved to be completely different.
For example, calling LoginClient.createNull().getUserInfo(...) should return a default response without actually talking to the third-party login service.
Nullables are production code and should be tested accordingly. Although Nulled instances are often used by tests, they’re also useful whenever you want the ability to “turn off” behavior in your application. For example, you could use Nullables implement a “dry run” option in a command-line application.
As another example, you can use Nullables in a web server to cache popular URLs when the server starts up:
If your Nullable reads data from external systems or state, or any of its dependencies do, implement Configurable Responses. If it or its dependencies write data, implement Output Tracking. If they respond to events, implement Behavior Simulation.
Embedded Stub
Nullables need to disable access to external systems and state while running everything else normally. The obvious approach is to surround any code that accesses the external system with an “if” statement, but that’s a recipe for spaghetti. Therefore:
When making code Nullable, don’t change your code. Instead, stub out the third-party code that accesses external systems.
In your stub, implement the bare minimum needed to make your code run. Ensure you don’t overbuild the stub by test-driving it through your code’s public interface. Put the stub in the same file as the rest of your code so it’s easy to remember and update when your code changes.
Write a stub of the third-party code, not your code, so your Sociable Tests test how your code will really work in production. Be careful to have your stub mimic the behavior of the third-party code exactly. To help you do so, write Narrow Integration Tests that document the behavior of the real code, paying particular attention to edge cases such as error handling and asynchronous code. Then write additional tests of the Nulled instance that will fail if your stub doesn’t have the same behavior.
Here’s a simple example of stubbing out JavaScript’s Math library:
Here’s a more complicated example. It stubs out Node.js’s http library:
Configure the embedded stub’s return values with Configurable Responses. If your language requires it, as with Java or C#, create a Thin Wrapper.
Nullables are production code, and despite appearances, the Embedded Stub is too. It must be tested accordingly. If you don’t like the idea of having stubs in your production code, you can put the Embedded Stub in a separate test-only file instead. However, this will make dependency management more complicated, and it will prevent you from using Nulled instances in production, which can be useful.
Thin Wrapper
Languages such as Java and C# will require your Embedded Stub to share an interface with the real dependency. Often, there won’t be an interface you can use, or it will be bigger than you need. Therefore:
Create a custom interface for your third-party dependency. Match the signature of the dependency exactly, but only include the methods your production code actually uses. Provide two implementations of the interface: a real version that only forwards calls to the third-party dependency, and an Embedded Stub.
If the third-party code returns custom types, you’ll need to wrap those return types as well. Remember to match the third-party code’s signatures exactly.
Configurable Responses
State-based tests of code with infrastructure dependencies needs to set up the infrastructure’s state, but setting up external systems is complicated and slow. Therefore:
Make the infrastructure dependencies Nullable and program the createNull() factory to take your desired response as an optional parameter. Define the responses from the perspective of the dependency’s externally-visible behavior, not its implementation.
If the Nullable dependency has multiple types of responses that can be configured, give each one its own configuration parameter. Use named and optional parameters so tests only need to configure the data they care about. If your language doesn’t support optional parameters, use an Options object, as shown in the Signature Shielding pattern.
For example, the following test is for a LoginController that depends on a Nullable LoginClient. Although LoginClient is used to make HTTP requests, its Configurable Responses aren’t about HTTP. Instead, they’re about the logged-in user’s email address and verification status, which is the behavior LoginController and its tests care about.
If it makes sense for your class to respond differently each time it’s called, configure the responses with an array or list. It’s often helpful to support two data types: a list of values, that results in a different response each time, and causes an exception when it runs out; and a single value, that returns the same response every time, and never runs out.
For example, the following test configures a Nullable DieRoller with a set of expected die rolls:
If your Nullable uses an Embedded Stub, implement the responses in the stub. Otherwise, Fake It Once You Make It. Either way, decompose the responses down to the next level.
The following example uses an Embedded Stub to make a random die roller. It’s configured at the level its callers care about: dice roll results. In the Embedded Stub, those configured values are decomposed to the level DieRoller operates at: random floating point numbers from between zero and one. For example, a configured roll of 6 is turned into the floating point number 0.83333.
The above code can be simplified by factoring #nextRoll() into a generic helper class. The result looks like this:
This is a JavaScript implementation of ConfigurableResponses you can use in your own code:
To test code with dependencies that write to infrastructure, use Output Tracking. To test code with dependencies that respond to events, use Behavior Simulation.
Output Tracking
State-based tests of code with dependencies that write to external systems need to check whether the writes were performed, but setting up external systems is complicated and slow. Therefore:
Program each dependency with a tested, production-grade trackXxx() method that tracks the otherwise-invisible writes. Have it do so regardless of whether the object is Nulled or not.
Track the writes in terms of the behavior your callers care about, not the underlying implementation of your code. For example, a structured logger might write strings to stdout, but its callers care about the structured data that’s being written. Its Output Tracking would track the data, not the string.
One way to implement Output Tracking is to have trackXxx() return an OutputTracker that listens for events emitted by your production code. The following example shows how this works, including implementations of OutputTracker in JavaScript and Java that you can use in your own code. It starts with a test of LoginPage, which writes to a structured Log when the user logs in.
At first glance, Output Tracking might look like the same thing as a spy (a type of test double), but there’s an important difference. Output Tracking records behavior, and spies record function calls. Output Trackers should write objects that represent the action that was performed, not just the function that was called to perform it. That way, when you refactor, you can change your functions without changing your Output Trackers or the tests that depend on them.
This is a JavaScript version of the OutputTracker class you can use in your own projects:
Below, I’ve also included a Java version of the OutputTracker library that I created with Ted M. Young. Because Java doesn’t have a built-in event emitter, it’s used slightly differently. Here’s an example of using it in the Log infrastructure wrapper from the earlier example:
This is the Java version of OutputTracker. Split it into two files.
Some external systems will push data to you rather than waiting for you to ask for it. Code that depends on those systems need a way to test what happens when their infrastructure dependencies generate those events, but setting up infrastructure to send events is complicated and slow. Therefore:
Add methods to your dependencies that simulate receiving an event from an external system. Share as much code as possible with the code that handles real external events. Write it as tested, production-grade code.
The following example consists of an Application-layer MessageServer that performs real-time networking. MessageServer runs on a server and connects to web browsers using WebSocketServer, a low-level infrastructure wrapper for Socket.IO. When a connected browser sends a message to MessageServer, it relays the messages to all the other connected browsers.
The test uses Behavior Simulation to simulate web browsers connecting and sending messages, then validates MessageServer’s behavior by using Output Tracking to confirm that the correct messages were sent.
The Behavior Simulation logic is implemented as production code in WebSocketServer, the low-level Socket.IO wrapper. Note how the real Socket.IO logic and the simulation methods share as much implementation as possible by delegating to #handleXxx() methods.
To test code with dependencies that read from infrastructure, use Configurable Responses. To test code with dependencies that write to infrastructure, use Output Tracking.
Fake It Once You Make It
Narrow Integration Tests are slow and difficult to set up. Similarly, Embedded Stubs can be difficult to create. Although they’re needed for low-level infrastructure wrappers, they’re overkill for code that doesn’t have direct dependencies on third-party infrastructure code. Therefore:
In application-layer code and high-level infrastructure wrappers, delegate to Nullable dependencies rather than using Narrow Integration Tests and Embedded Stubs. In your tests, inject Nulled instances of the code under test’s dependencies. If your production code has a createNull() factory, implement it by creating Nulled dependencies and decomposing your Configurable Responses into the format your dependencies expect.
For example, the following code tests LoginClient, which depends on a low-level HttpClient. The LoginClient tests Fake It Once You Make It by injecting a Nulled version of HttpClient into LoginClient.
The production LoginClient code is a wrapper for Auth0, an authentication service. LoginClient.createNull() has Configurable Responses related to authentication, such as configuring the login email address. They’re implemented by creating a Nulled HttpClient and decomposing LoginClient’s Configurable Responses into the actual HTTP responses Auth0 would return.