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)