technical skills - testing without mocks - example
Here’s an example of testing a simple command line application. The application reads a string from the command line, encodes it using ROT-13, and outputs the result.
The production code uses the optional A-Frame Architecture pattern. App
is the application entry point. It depends on Rot13
, a Logic class, and CommandLine
, an Infrastructure class. Additional patterns are mentioned in the source code.
// Example production code (JavaScript + Node.js)
import CommandLine from "./infrastructure/command_line"; // Infrastructure Wrapper
import * as rot13 from "./logic/rot13";
export default class App {
constructor(commandLine = CommandLine.create()) { // Parameterless Instantiation
this._commandLine = commandLine;
}
run() {
const args = this._commandLine.args();
if (args.length === 0) { // Tested by Test #2
this._commandLine.writeOutput("Usage: run text_to_transform\n");
return;
}
if (args.length !== 1) { // Tested by Test #3
this._commandLine.writeOutput("too many arguments\n");
return;
}
// Tested by Test #1
const input = args[0]; // Logic Sandwich
const output = rot13.transform(input);
this._commandLine.writeOutput(output + "\n");
}
};
The tests of App
look like end-to-end integration tests, but they’re actually unit tests. Technically, they’re Narrow, Sociable tests, which means they’re unit tests that execute code in dependencies.
As narrow tests, the tests only care about testing App.run()
. Each of the dependencies is expected to have tests of their own, which they do.
The tests use a Nullable CommandLine
to throw away stdout
and Configurable Responses to provide pre-configured command-line arguments. They also use Output Tracking to see what would have been written to stdout
.
// Example tests (JavaScript + Node.js)
import assert from "assert";
import CommandLine from "./infrastructure/command_line";
import App from "./app";
describe("App", () => {
// Test #1
it("reads command-line argument, transform it with ROT-13, and writes result", () => {
const { output } = run({ args: [ "my input" ] }); // Signature Shielding, Configurable Responses
assert.deepEqual(output.data, [ "zl vachg\n" ]; // Output Tracking
});
// Test #2
it("writes usage when no argument provided", () => {
const { output } = run({ args: [] }); // Signature Shielding, Configurable Responses
assert.deepEqual(output.data, [ "Usage: run text_to_transform\n" ]); // Output Tracking
});
// Test #3
it("complains when too many command-line arguments provided", () => {
const { output } = run({ args: [ "a", "b" ] }); // Signature Shielding, Configurable Responses
assert.deepEqual(output.data, [ "too many arguments\n" ]); // Output Tracking
});
function run({ args = [] } = {}) { // Signature Shielding
const commandLine = CommandLine.createNull({ args }); // Nullable, Infrastructure Wrapper, Configurable Responses
const output = commandLine.trackOutput(); // Output Tracking
const app = new App(commandLine);
app.run();
return { output }; // Signature Shielding
}
});
If you’re familiar with mocks, you might assume CommandLine
is a test double. But it’s actually production code with an “off” switch and the ability to monitor its output.
// Example Nullable infrastructure wrapper (JavaScript + Node.js)
import EventEmitter from "node:events";
import OutputTracker from "output_tracker";
const OUTPUT_EVENT = "output";
export default class CommandLine {
static create() {
return new CommandLine(process); // 'process' is a Node.js global
}
static createNull({ args = [] } = {}) { // Parameterless Instantiation, Configurable Responses
return new CommandLine(new StubbedProcess(args)); // Embedded Stub
}
constructor(proc) {
this._process = proc;
this._emitter = new EventEmitter(); // Output Tracking
}
args() {
return this._process.argv.slice(2);
}
writeOutput(text) {
this._process.stdout.write(text);
this._emitter.emit(OUTPUT_EVENT, text); // Output Tracking
}
trackOutput() { // Output Tracking
return OutputTracker.create(this._emitter, OUTPUT_EVENT);
}
};
// Embedded Stub
class StubbedProcess {
constructor(args) {
this._args = args; // Configurable Responses
}
get argv() {
return [ "nulled_process_node", "nulled_process_script.js", ...this._args ];
}
get stdout() {
return {
write() {}
};
}
}
The patterns shine in more complex code that has multiple layers of dependencies. Find more examples here:
- Simple example. The complete source code for the above example. (JavaScript or TypeScript with Node.js)
- Complex example. The blinged-out version of the above example. A web application and microservice that performs ROT-13 encoding. Production-grade code with error handling, logging, timeouts, and request cancellation. (JavaScript or TypeScript with Node.js)
- TDD Lunch & Learn Screencast. A series of one-hour webinars that demonstrate how to use the patterns. (JavaScript with Node.js)
- Nullables Livestream. A series of three-hour livestreams with James Shore and Ted M. Young. They pair on applying the patterns to an existing web application. (Java with Spring Boot)