Imagine a Mocha test file like this
1 | describe('my tests', function () { |
The single test named "works" is synchronous; it passes
1 | $ cat package.json |
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 | describe('my tests', function () { |
1 | $ npm t |
Great. What if the test takes 3 seconds?
1 | it('passes after 3000ms', function (done) { |
1 | $ npm t |
Mocha uses 2 second test limit by default. Let us increase the timeout to 3.5 seconds in that one test.
1 | it('passes after 3000ms', function (done) { |
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 | it('passes after 3000ms', function (done) { |
1 | test arguments { '0': [Function] } |
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 | it('passes after 3000ms', function (done) { |
1 | TypeError: Converting circular structure to JSON |
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 | test arguments { '0': [Function] } |
Ok, let us print the keys of the object
1 | // inside the test |
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 | it('passes after 3000ms', function (done) { |
1 | test arguments { '0': [Function] } |
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 | it('passes after 3000ms', (done) => { |
1 | test arguments {} |
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 | const allTests = [] |
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!
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 | describe('my tests', () => { |
1 | $ npm t |
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 | function foo () { |
1 | test arguments {} |
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 |
|
1 | test arguments {} |
But: if we do not use our outside foo
function, using strict
mode has no effect!
1 |
|
1 | test arguments { '0': {}, |
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 | // index.js |
Notice how we are passing arguments to foo
and bar
. What are the arguments
inside
bar
arrow function?
1 | $ node index.js |
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 | const source = fs.readFileSync(filename, 'utf8') |
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
5describe('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 recommendationsthis
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.
Please avoid the eternal suffering by using functional programming with its emphasis of pure functions.