In an attempt to extend the functionality of Mocha for a specific use case I was required to look into its source code.
I wanted to write the flow down so it was clearer to myself. Maybe someone else will find this useful.
Mocha
Firstly you have the Mocha
'object' which in many respects is a container for all the other constituent parts (outlined below).
An instance of Mocha
is exposed as mocha
. The mocha instance has some helper methods for setting up Mocha with the correct options. For example the interface with which you will write your tests (BDD/TDD/qUnit).
One method on the Mocha prototype is run
. Calling this instantiates a Reporter
and a Runner
(amongst other things).
Runner
A Runner
object runs yours tests. Mocha by default creates a base level suite which the runner is instantiated with.
Each suite requires a Context
. This context references a Runnable
, and provides various prototype methods to set context specific settings. Each Test
is a Runnable
, as is each Hook
.
When you call mocha.run
, it creates this runner, sets various things up and calls its run
prototype method.
Reporter
A reporter programatically specifies how the test output will be shown. A suprising amount of Mocha's codebase is different types of reporter. You can output your results in 'Nyan Cat' format if you so wish :)
On creation the Mocha object loads a reporter based on the passed options. When you run mocha the Reporter
instance is instantiated, passing the Runner
in.
Hooks
Hooks are hooks. They are pieces of code that can be hooked in at different stages of the process. Mocha has hooks such as before()
, after()
, beforeEach()
, and afterEach()
.
Behind the scenes, they are set up in exactly the same way as Tests.
Running your tests
Mocha relies heavily on nodes EventEmitter
to pass messages around. For example it is used to tell the reporter that the test suite has started running. When we run our Runner
it emits a message 'start'. I wont explicitly mention these messages after this point - essentially at each stage of the execution an appropriate message is sent/received/acted upon.
When we call mocha.run
a UI is setup. The default is the BDD (behaviour driven development) UI. This essentially defines a number of methods.. namely describe()
, it()
, and the various hook methods. These are what you use to write your tests.
describe
When you call describe a new suite is created. What Mocha does here (if I have understood correctly) to allow for nested suites is very very clever.
Javascript executes syncronously, line after line. Within describe, a suite is created using the suite at the front of the locally available suites
array as its parent. Once a suite has been created it is added to the start of this array.
It then executes the passed in function body of the describe
call. If it has nested describe calls, these now use the new suites[0]
as their parent when the respective suite is created.
After the function has completed executing, the suite is removed from the suites
array.
Another interesting tidbit is that suites inherit their parents context (ctx
).
var context = function() {};
context.prototype = parentContext;
this.ctx = new context();
This allows them to inherit timeout settings amongst other things.
it
When you call it, a new Test
is created and this is added to the suite. it()
is executed in the context of its suite by using call()
from within describe()
.
The Runner
has a runSuite
method which is called with Mocha's initial base suite. This method runs the appropriate 'suite level' Hooks, and then runs any tests (runTests
). runTests
calls the beforeEach
hooks, which on completion runs the test, which on completion runs the afterEach
hooks, which on completion runs the next test. Once all the tests are completed the callback from the level above is executed which will recursively execute runSuite
on any nested suites.
The runTests
method does similar in the sense that it runs the respective hooks at the right times (beforeEach
, and afterEach
). afterEach
is passed a callback which on completing the hook is executed. This callback runs the next test allowing for test recursion.
The running of a test involves calling the run
method on the test object.
The test object inherits this method from Runnable
. Runnable
has the method run
which sets the test as the runnable within the Context
object (initially set at the very lowest level in the instantiation of Mocha
).
Now.. one of the great features of Mocha is that you can have tests running asynchronous code. The way this works is simple. I initially suspected that this would be done with timers and regular polling. It is in fact a lot cleaner and simpler than that.
The execution of sequential tests as mentioned above is controlled by its previous test in that within the call Test.run(fn)
, fn
is a callback which executes the following test. As such the Runnable
run
method simply does not execute this callback until it is complete (you have triggered done()
) or until the test times out.
Reporting
So we have written our tests and can run them. At each step of the way we emit messages using nodes EventEmitter
.
A reporter, in extremely simple terms, listens to these messages and produces an output.
In addition to that, the reporters format the output so that it is useful. Mocha comes with jsDiff so you can see visual diffs where appropriate. They also control important things like the colours of your output, and printing out cat faces..
Further reading
Mocha is quite complex under the hood, and makes use of a number of complex yet clever design patterns. This is a general more in depth overview of its internals. The next step is to read the source code - explaining all the fine details in written text would be more challenging than the actual concepts.
If anything is not clear please do let me know.