~ $ cd craft/a-test-is-an-example && cat README.md

A test
is an example
of use.

One of the first essays I published, in 2018, has an example that begins with two tests of a dispatcher that pass, cover every line of it, and say almost nothing: they look at its _queue, a part meant to be private. It ends with one test written the way the dispatcher would be documented — add a listener, deliver a message, the listener gets it.

Here are the three, run against a dispatcher they all pass, against the same dispatcher refactored, and against one with a bug in it. Choose one:

Original

dispatcher.js — original
class Dispatcher {
  _queue = [];

  addListener(cb) {
    this._queue.push(cb);
  }

  deliver(message) {
    this._queue.forEach((cb) => cb(message));
  }
}

The dispatcher the three tests imply: its listeners kept in an array, _queue.

Looking inside

  • addListener should add a callback to the queue
  • deliver should invoke queue callbacks with the received argument

Reading like documentation

  • delivers messages to listeners

Refactored

dispatcher.js — refactored
class Dispatcher {
  #listeners = [];

  addListener(cb) {
    this.#listeners.push(cb);
  }

  deliver(message) {
    this.#listeners.forEach((cb) => cb(message));
  }
}

The same array, renamed and made private. Nothing a user of it can see has changed.

Looking inside

  • addListener should add a callback to the queueExpected: something containing [MockFunction]. Received: undefined.
  • deliver should invoke queue callbacks with the received argumentTypeError: Cannot read properties of undefined (reading 'push')

Reading like documentation

  • delivers messages to listeners

With a bug

dispatcher.js — with a bug
class Dispatcher {
  _queue = [];

  constructor() {
    const queue = this._queue;
    this.deliver = (message) => queue.forEach((cb) => cb(message));
  }

  addListener(cb) {
    this._queue = [...this._queue, cb];
  }
}

addListener now copies the queue instead of changing it, and deliver, bound once in the constructor, still reads the first one.

Looking inside

  • addListener should add a callback to the queue
  • deliver should invoke queue callbacks with the received argument

Reading like documentation

  • delivers messages to listenersExpected: called with "message". Received: never called.

Refactored, nothing a user of the dispatcher could see has changed, and the two tests that looked inside break anyway. With the bug, both of them stay green: each checks its own half against _queue, and the bug is where the halves meet. Only the one that reads like documentation is right both times. In Kent Beck's words: “Tests should be coupled to the behavior of code and decoupled from the structure of code.”

So I write tests the way the thing would be explained to someone about to use it, and a test is also the example that someone can copy. Three of the rules I ended that essay with still hold:

The essay is Why you should start writing tests as they were documentation.

~/craft/a-test-is-an-example $

~/craft/a-test-is-an-example $