Tests, closures and arrow functions

Using Mocha test context and its pitfalls.

Imagine a Mocha test file like this

spec.js
1
2
3
4
5
describe('my tests', function () {
it('works', function () {
// everything works fine
})
})

The single test named "works" is synchronous; it passes

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ cat package.json
{
"name": "test-closures",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "mocha spec.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"mocha": "^3.5.0"
}
}
$ npm it
> mocha spec.js

my tests
✓ works

1 passing (9ms)

What if the test is asynchronous? We need to either return a promise or accept done test callback parameter. Let us use done - it is simpler to call from a setTimeout

1
2
3
4
5
6
7
8
describe('my tests', function () {
it('works', function () {
// everything works fine
})
it('passes after 500ms', function (done) {
setTimeout(done, 500)
})
})
1
2
3
4
5
6
7
8
$ npm t
> mocha spec.js

my tests
✓ works
✓ passes after 500ms (502ms)

2 passing (512ms)

Great. What if the test takes 3 seconds?

1
2
3
it('passes after 3000ms', function (done) {
setTimeout(done, 3000)
})
1
2
3
4
5
6
7
8
$ npm t
1) passes after 3000ms
2 passing (3s)
1 failing

1) my tests passes after 3000ms:
Error: Timeout of 2000ms exceeded. For async tests and hooks, ensure "done()" is called;
if returning a Promise, ensure it resolves.

Mocha uses 2 second test limit by default. Let us increase the timeout to 3.5 seconds in that one test.

1
2
3
4
it('passes after 3000ms', function (done) {
this.timeout(3500)
setTimeout(done, 3000)
})
1
✓ passes after 3000ms (3002ms)

Great, the test is passing. Does Mocha pass anything else into the test callback function? We can check by printing arguments

1
2
3
4
5
it('passes after 3000ms', function (done) {
console.log('test arguments', arguments)
this.timeout(3500)
setTimeout(done, 3000)
})
1
2
test arguments { '0': [Function] }
✓ passes after 3000ms (3010ms)

Seems, the test callback only gets the done parameter and nothing else. Are there any other methods the test callback can call on its context besides this.timeout? Let us print the this variable inside the test.

1
2
3
4
5
6
it('passes after 3000ms', function (done) {
console.log('test arguments', arguments)
console.log('this', this)
this.timeout(3500)
setTimeout(done, 3000)
})
1
2
3
4
5
6
7
TypeError: Converting circular structure to JSON
at Object.stringify (native)
at formatValue (util.js:352:36)
at inspect (util.js:186:10)
at exports.format (util.js:130:20)
at Console.log (console.js:43:37)
at Context.<anonymous> (spec.js:10:13)

Hmm, not good. If we try printing using console.log('this %j', this) we are not getting much more information, but at least we are not crashing

1
2
test arguments { '0': [Function] }
this [Circular]

Ok, let us print the keys of the object

1
2
// inside the test
console.log('this', Object.keys(this))
1
this [ '_runnable', 'test' ]

We are getting something! The test property is especially interesting. It has the name, the test callback and other properties describing the current test.

1
2
3
4
5
6
it('passes after 3000ms', function (done) {
console.log('test arguments', arguments)
console.log('this.test', this.test)
this.timeout(3500)
setTimeout(done, 3000)
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
test arguments { '0': [Function] }
this.test {
"title": "passes after 3000ms",
"body": "function (done) {\n console.log('test arguments', arguments)\n ... }",
"async": 1,
"sync": false,
"timedOut": false,
"pending": false,
"type": "test",
"file": "/test-closures/spec.js",
"parent": "#<Suite>",
"ctx": "#<Context>",
"timer": {}
}

Via this.test we have access to the test's code (this.test.body), the test title, its file, its parent suite of tests, etc. This comes in very handy when extending Mocha with snap-shot testing for example.

Test closures

But what happens if we get tired of writing "long" callback functions and instead use arrow functions?

1
2
3
4
5
6
it('passes after 3000ms', (done) => {
console.log('test arguments', arguments)
console.log('this.test', this.test)
this.timeout(3500)
setTimeout(done, 3000)
})
1
2
3
4
5
test arguments {}
this.test undefined
1) passes after 3000ms
Error: Timeout of 2000ms exceeded. For async tests and hooks, ensure "done()" is called;
if returning a Promise, ensure it resolves.

Everything breaks! Why is this still an object, but this.timeout has no effect, and the property this.test is undefined?

When you use a "normal" callback function, Mocha creates a Test instance and binds it as this when calling your callback. It could be something like this behind the scene

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const allTests = []
function it(name, cb) {
const test = new Mocha.Test({
name: 'passes after 3000ms',
body: cb.toString(),
cb: cb,
timeout: (ms) => {
// change current test's time limit
}
})
allTests.push(test)
}
// running all tests
allTests.forEach(test => {
const done = () => {
// continue current test
}
test.cb.call(test, done)
})

By using test.cb.call(test, ...) the test runner sets this inside the test callback function to the full "Mocha.Test" instance. What happens when you use arrow function as a test callback? The arrow functions bind the this context to whatever was outside their closure. If you are unsure what JavaScript closures are, read this blog post. In our example, inside the callback this will be whatever it was outside the callback's source code in our spec file, which is "describe" callback function!

This inside the arrow test callback

The function surrounding our test arrow callback as written in the spec.js file is the describe callback "full" function. Mocha test runner creates a special context when executing each describe callback, thus the spec, instead of proper Mocha.Test instance gets something like Mocha.Describe instance! This leads to the confusion and produces the dummy this.timeout method that does nothing.

Even worse, what happens if the describe function uses arrow function as callback?

1
2
3
4
5
6
7
8
describe('my tests', () => {
it('passes after 3000ms', (done) => {
console.log('test arguments', arguments)
console.log('this.test', this.test)
this.timeout(3500)
setTimeout(done, 3000)
})
})
1
2
3
4
5
6
7
8
9
$ npm t
test arguments { '0': {},
'1':
{ [Function: require]
resolve: [Function: resolve],
... lots of text
this.test undefined
1) passes after 3000ms
TypeError: this.timeout is not a function

That is unexpected. The this.timeout() call used this which due to arrow function callback points at this inside the describe callback; which itself points outside because it is a callback function. When you point outside the outer function what do you get? In JavaScript this differs. If you are inside a proper function, the outside context would be a global object (Node) or window object (browser). So if we wrap our describe in a dummy function foo, we would get this === global inside each test.

1
2
3
4
5
6
7
8
9
10
11
function foo () {
describe('my tests', () => {
it('passes after 3000ms', (done) => {
console.log('test arguments', arguments)
console.log('this === global', this === global)
// this.timeout(3500)
setTimeout(done, 3000)
})
})
}
foo()
1
2
test arguments {}
this === global true

My general advice when dealing with scope madness like this (no pun intended) is to use the strict mode to prevent default context pointing at global

1
2
3
4
5
6
7
8
9
10
11
12
13
'use strict'
function foo () {
describe('my tests', () => {
it('passes after 3000ms', (done) => {
console.log('test arguments', arguments)
console.log('this', this)
console.log('this === global', this === global)
// this.timeout(3500)
setTimeout(done, 3000)
})
})
}
foo()
1
2
3
test arguments {}
this undefined
this === global false

But: if we do not use our outside foo function, using strict mode has no effect!

1
2
3
4
5
6
7
8
9
10
'use strict'
describe('my tests', () => {
it('passes after 3000ms', (done) => {
console.log('test arguments', arguments)
console.log('this', this)
console.log('this === global', this === global)
// this.timeout(3500)
setTimeout(done, 3000)
})
})
1
2
3
4
5
6
7
8
9
test arguments { '0': {},
'1':
{ [Function: require]
resolve: [Function: resolve],
... lots of text
'3': '/test-closures/spec.js',
'4': '/test-closures' }
this {}
this === global false

I think I can speak for everyone when I say "WTF".

What is this empty context {} object we are getting? What is this huge arguments object we are seeing in the arrow function? Why does everyone have to be so complicated?

Well, it is still due to the JavaScript closure scope rules. First, about the weird arguments object. When you use the arrow function you "lose" your immediate arguments and instead your arguments points at the first full closure function's arguments object!

1
2
3
4
5
6
7
8
9
// index.js
function foo() {
const bar = (myArg) => {
console.log('inside bar arguments', arguments)
console.log('myArg', myArg)
}
bar('bar')
}
foo(1, 2, 3)

Notice how we are passing arguments to foo and bar. What are the arguments inside bar arrow function?

1
2
3
$ node index.js 
inside bar arguments { '0': 1, '1': 2, '2': 3 }
myArg bar

They are arguments of foo()! Ok, a little crazy, but I guess if this points at the outside full function's closure, arguments might as well. So what are the magical 5 arguments our spec callback function got? Where are they coming from? Well, this is from Node's require function (for full code example see Hacking Node require). Every time a JS file is loaded by Node, it does the following

1
2
3
const source = fs.readFileSync(filename, 'utf8')
const module = eval('(function (exports, require, module, __filename, __dirname) { ' +
source + '\n}()')

The require wraps the spec.js in a full function, passing 5 parameters - that is where "magical" variables __filename and __describe are coming from! If we do not have a proper function inside out tests of our own, the arrow functions "find" the outside function from require and use its context (bypassing use strict command) and even getting its arguments object.

What a mess. And all because the Mocha test runner uses this to let the test code set its time limit.

Final thoughts

A couple of points to finish this discussion.

  • Whenever I need a custom timeout in one of my test callbacks, I make sure to use "proper" callback function.

    1
    2
    3
    4
    5
    describe('my tests', () => {
    it('passes after 3000ms', function (done) {
    this.timeout(3500)
    })
    })
  • Other test frameworks like Tape and Ava avoid using this and pass you and explicit argument. Simple and safe, see my test framework recommendations

  • this keyword in JavaScript will burn you one day. Then it will burn you again and again and again. If Dante Alighieri were alive today, he would put writing object-oriented JavaScript among one of the first levels of Hell for sure.

Dante pointing at JavaScript developers

Please avoid the eternal suffering by using functional programming with its emphasis of pure functions.