Dependency injection vs IO Monad example

How to wrap functions that depend on the environment for clean unit testing.

I have a small CLI utility for outputting source files centered in the terminal. The module center-code is very useful during demos or presentations - the centered code looks great projected on the projected screen when the font is large.

center code example

To center text, I grab the terminal resolution using the following NodeJS code

1
2
3
4
5
6
7
8
9
10
11
// somewhere in https://github.com/bahmutov/center-code/blob/master/index.js
function terminalSize() {
if (process.stdout &&
isNumber(process.stdout.columns) &&
isNumber(process.stdout.rows)) {
return {
width: process.stdout.columns,
height: process.stdout.rows
};
}
}

The code works and can be unit tested, for example I wrote the following

spec/misc-spec.js
1
2
3
4
5
6
it('works under Node', function () {
var resolution = terminalSize();
la(resolution, 'got resolution object', resolution);
la(typeof resolution.width === 'number', 'has width', resolution);
la(typeof resolution.height === 'number', 'has height', resolution);
});

You can find this unit test in the spec/misc-spec.js.

The tests pass when I run Mocha from the command line.

$ npm run test
> [email protected] test /git/center-code
> mocha spec/*-spec.js
  widest(lines)
    ✓ finds longest line length
  blanks(n)
    ✓ forms empty string with 5 spaces
  terminalSize()
    ✓ works under Node
  3 passing (27ms)

All was good, I was happy and tried to commit this code. Before each commit, I run npm test and other commands using a pre-git hook

package.json
1
2
3
4
5
6
"pre-commit": [
"npm test",
"npm run example",
"npm run modules-used",
"npm version"
]

So I was trying to commit, but the hook failed!

$ g done "adding unit test for grabbing terminal resolution"
executing task "npm" with args "test"
> [email protected] test /git/center-code
> mocha spec/*-spec.js
  widest(lines)
    ✓ finds longest line length
  blanks(n)
    ✓ forms empty string with 5 spaces
  terminalSize()
    1) works under Node (dirty)
  2 passing (30ms)
  1 failing
  1) terminalSize() works under Node (dirty):
     Error: got resolution object undefined

What is going on? Turns out when the pre-commit hook runs, it spawns a shell that is not a human terminal. Thus it will not have columns or rows properties. Our code thus depends on the outside state, and we just got burnt.

Mock dependency injection solution

We can work around it the problem by injecting the output stream variable dynamically, and using a mock value inside unit tests. Note that the function terminalSize became pure - it only uses its arguments to compute the result.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function terminalSize(outputStream) {
if (outputStream &&
isNumber(outputStream.columns) &&
isNumber(outputStream.rows)) {
return {
width: outputStream.columns,
height: outputStream.rows
};
}
}
function centerText(options) {
// the run time code can be non-pure
var size = terminalSize(process.stdout);
...
}

The unit test sends a mock output stream into the function.

misc-spec.js
1
2
3
4
5
6
7
8
9
10
it('works under Node', function () {
var fakeTerminal = {
columns: 20,
rows: 10
};
var resolution = terminalSize(fakeTerminal);
la(resolution, 'got resolution object', resolution);
la(resolution.width === fakeTerminal.columns, 'has width', resolution);
la(resolution.height === fakeTerminal.rows, 'has height', resolution);
});

We can even confirm the output width and height numbers by comparing them to the fake input terminal's dimensions.

Monad solution

There is another way to solve the "dangerous" function problem. We can postpone the execution of the function that needs global variables until the very last moment. For example, our dangerous function is the one that returns the process object. Let us write it.

1
2
// impure function - danger: using and testing requires care
function getProcess() { return process; }

The function that computes the terminal resolution instead of using the argument will return an IO monad object, composing the unsafe getProcess function with getting the columns and rows.

1
2
3
4
5
6
7
8
9
10
11
12
13
function getProcess() { return process; }
function terminalSize() {
var R = require('ramda');
var IO = require('./src/io');
return new IO(getProcess)
.map(R.prop('stdout')) // get the 'stdout' property
.map(function (outputStream) {
return {
width: outputStream.columns,
height: outputStream.rows
};
});
}

Notice that the function terminalSize is pure - it never accesses anything outside its arguments. Yes it has a reference to the dangerous getProcess stored inside the IO object, but it is never executed!

The IO constructor is a small utility function taken from Chapter 8 of the Mostly adequate guide to FP in JavaScript. All it does is to

  1. Holds reference to a (possibly dangerous) function inside.
  2. Has .map method to create a composition of the function inside with a given callback function.

The composition using .map returns a new IO instance, building a chain of functions that are NOT executed until we call the inner function. Here is the code for IO

io.js
1
2
3
4
5
6
7
8
var R = require('ramda'); // only need R.compose
var IO = function(f) {
this.unsafePerformIO = f;
}
IO.prototype.map = function(f) {
return new IO(R.compose(f, this.unsafePerformIO));
}
module.exports = IO;

By holding the unsafe function, and later, a composition of unsafe function with other functions inside an object, and providing a general .map method, we got ourselves a Monad. But the name does not matter - only using it matters. For example we can do something like this to delay calling an unsafe function until we are ready

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var ioGlobal = new IO(function unsafeGlobalAccess() {
// UNSAFE - returns outside variable that might not exist
return global;
});
function getArguments(glob) {
// pure function - only uses the argument
return glob.process.argv;
}
function checkArgs(args) {
// pure function too
console.assert(Array.isArray(args), 'arguments is a list');
}
// create a chain of functions
// BUT nothing is called yet. Including the "dirty"
// function unsafeGlobalAccess that returns the global state
var monad = ioGlobal
.map(getArguments)
.map(checkArgs);
// start the computation. Now unsafeGlobalAccess runs
monad.unsafePerformIO();

Pretty cool!

After writing IO monad code, let us rewrite our imperative code that computes the padding for the given text to be in the terminal's center using an IO monad chain. Our original code was

1
2
3
4
5
6
7
8
9
10
function centerText(options, source) {
var size = terminalSize(process.stdout);
log('terminal %d x %d', size.width, size.height);
var sourceSize = textSize(source);
log('source size %d x %d', sourceSize.columns, sourceSize.rows);
var paddedHorizontally = padHorizontally(size, source);
var paddedVertically = padVertically(size, paddedHorizontally);
var highlighted = highlight(options.filename, paddedVertically);
console.log(highlighted);
}

Our modified code is almost the same.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function centerText(options, source) {
var monad = terminalSize()
.map(function center(size) {
log('terminal %d x %d', size.width, size.height);
var sourceSize = textSize(source);
log('source size %d x %d', sourceSize.columns, sourceSize.rows);
var paddedHorizontally = padHorizontally(size, source);
var paddedVertically = padVertically(size, paddedHorizontally);
var highlighted = highlight(options.filename, paddedVertically);
console.log(highlighted);
});
// nothing has happened yet - no functions executed, just composed
// now run them (including unsafe ones)
monad.unsafePerformIO();
}

Notice that the function centerText is pure, and nothing bad happens until the function inside the monad is executed. Only then the entire pipeline starts: first the unsafe getProcess runs, then the terminal size is computed, then the size is passed into the center callback.

Now to our unit test, it is changing a little to set mock property before running the monad chain.

1
2
3
4
5
6
7
8
9
10
11
12
it('works with a monad', function () {
var monad = terminalSize()
.map(function checkTerminal(size) {
la(size.width === 42);
la(size.height === 20);
});
// nothing ran yet. Time to prepare the environment!
process.stdout.columns = 42;
process.stdout.rows = 20;
// now start the monad execution
monad.unsafePerformIO();
});

We could have restored the original terminal dimensions, but I skipped this for clarity.

To learn more about monads and other functional goodness, read the Mostly adequate guide to FP (in javascript).