~ $ 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
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 queuedeliver should invoke queue callbacks with the received argument
Reading like documentation
delivers messages to listeners
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
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 queuedeliver 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:
- “Never use ‘private’ properties in testing code.”
- “Test functionalities, not functions/methods.”
- “Verify that testing code should be copy-pasteable to new production code.”
The essay is Why you should start writing tests as they were documentation.