Intrusive Unit Testing
I learned a technique from an experienced Perl programmer that I’ve started using in all scripting languages, like Python, shell, and JavaScript: interspersing unit tests with definitions. I call this intrusive unit testing1 I don’t know if this technique has another established name. I call it intrusive unit testing by analogy to intrusive linked lists, where the pointer to the next element gets embedded inside the data structure it refers to..
This is an example of what it might look like, from a small 2D vector library I created for a project.
import * as test from './testing.js'; export function V2(x, y) { return { x, y, sub: _sub, l2norm: _l2norm, }; } test.ok(V(5, 8), 'Vector creation'); test.is(V(5, 8).x, 5, 'X assignment'); test.is(V(5, 8).y, 8, 'Y assignment'); function _sub(other) { return new V2(this.x - other.x, this.y - other.y); } { const v = V2(5, 8).sub(V2(3, 12)); test.is([v.x, v.y], [2, -4], 'Subtraction'); } function _l2norm() { return Math.sqrt(this.x**2 + this.y); } test.is(V2(3, 4).l2norm(), 5, 'L2 norm'); test.done();
Whenever this module is imported, or the script runs, it prints to standard output:
ok 1 - Vector creation ok 2 - X assignment ok 3 - Y assignment ok 4 - Subtraction not ok 5 - L2 norm # found 3.6055512, expected 5 1..5 # Done testing. 4 passed, 1 failed.
And it’s clear that the implementation of the L2 norm is broken – in this case I forgot to square the Y component.
This is all very simple, but it works surprisingly well. A script is usually executed from top to bottom, and the last thing in the script is often a call to a “main” function. Since any code between definitions is executed in order before the call to the main function, a script with intrusive unit tests always starts by running its full test suite!
If any test fails, we can choose to skip the main entrypoint and just exit the script instead. That way, the script really only runs when its test suite passes.
The alternative to intrusive unit testing would be setting up a separate test suite, but that to me always feels like a bigger investment. Intrusive unit testing allows us to “just call the function and assert the result” – all the benefits of testing with few of the costs.
The functions to perform the actual assertions can be quite simple. Here’s how one can write them in JavaScript2 There’s a lot of repetition going on that could be abstracted but my point is that it’s literally just three functions with a handful of lines of code in each plus two state variables.:
let _test_number = 1; let _failures = 0; export function ok(actual, descr) { const msg = descr ? ` - ${descr}` : ''; if (actual) { console.log(`ok ${_test_number++}${msg}`); } else { _failures++; console.error(`not ok ${_test_number++}${msg}`); console.error(`# found ${actual}, expected truthy`); } } export function is(actual, expected, descr) { const msg = descr ? ` - ${descr}` : ''; if (JSON.stringify(actual) == JSON.stringify(expected)) { console.log(`ok ${_test_number++}${msg}`); } else { _failures++; console.error(`not ok ${_test_number++}${msg}`); console.error(`# found ${actual}, expected ${expected}`); } } export function done() { const success = _test_number - 1 - _failures; console.log( `1..${_test_number - 1} # Done testing. ` + `${success} passed, ` + `${_failures} failed.` ); return _failures; }
I have these functions written in a few different scripting languages and I have made a point of not being afraid to copy-paste them into scripts where might be useful.
The shape of the output conforms to a simple subset of the Test Anything Protocol, which means it should be recogniseable by test harnesses, if at some point the script goes through a ci system.
That’s it! You now have no excuse not to add automated tests to your scripts as they grow. Use intrusive unit testing and most of the work takes care of itself.