Minimal Mochajs testing framework DSL
A minimal mochajs testing framework DSL, in less than 100 lines of JavaScript.
Often, implementing essence of an idea helps a lot with understanding it. This is an attempt at understanding mocha like DSL. Code at github
DSL details
Lets define a Mocha TDD like DSL, with 4 commands.
describe(desc, fn)setup(fn)teardown(fn)it(desc, fn)// `describe` could be nested within `describe`, to arbitrary depth. ex:describe('top level describe', function (){// ...describe('nested describe', function () {// ...describe('nested nested describe', function () {// ...})// ...});// ...});
Each describe could have setup
, teardown
, it
and well describe
calls.
All functions registered to setup
should be run first. Then all the
tests registered with it
, and then all the nested describe
have to
be executed. After all that, function registed to teardown
should be
executed. Ex:
- Setup
- Execute all the test in the level
- Execute all nested levels
- Teardown
So, how do we solve it.
Lets first think just one level (ex: ignore nested describes). We
would have to collect all the DSL commands at that level, and then
execute them in a specific order. Note that we can't just execute the
commands in the order we come accros. Example, a setup
might come
after tests (it
). But all the setup
in a level have to be run
before the tests. The way I acheive this is to define all the DSL
commands as functions which save the command, and its arguments, to be
executed later. ex: setup(fn)
would save a pair ["setup", fn]
. ex:
// 'key' being the dsl command, and 'val' argumentsfunction spush(key, val){stack[stack.length-1][key].push(val);}
We will come to the stack
part later. Now, once all the commands in
a describe level are collected, they are to be executed one by one.
Execution of setup
and teardown
commands is simple. Just execute
the function registered with them, in the context (there is a single
context, on which all setup and teardown work). Test (functions
registered with it
) are executed by running the registered function,
and showing success or failure based on output. For now, it just
checks for the exception thrown by the function. Ex:
function reportTests(fn, desc) {desc = test_title(stack, desc);try {fn.call(ctx);success(desc);} catch(e) {failure(desc, e.message);}}
Describe is the special case. Executing it, essentially means doing
the above, at the nested level (ex: the function registered with the
describe
). Note that teardown
should run at the end. So, some
information about the current level has to be saved, till all the
nested levels are done. This is the reason to have stack, to keep the
commands. describe
is kinda the main function of the DSL.
function exec_describe(title, tfn) {stack.push(new_top(title));// collect 'describe', 'setup', 'teardown' and 'it' for the// current leveltfn.call(ctx);exec_top(); // execute themstack.pop();}
Well, thats mostly it. There are some small details. Like directly
executing the describe
the first time we come across it (the process
has to start somewhere). Or printing the titles etc.
Full code below.
const success = desc => console.log(`${desc} : Pass`);const failure = (desc, msg) => console.log(`:( ${desc} : Fail => ${msg}`);const log = desc => console.log(desc);const activity = {};const stack = [];const isEmptyStack = () => stack.length === 0;const stackTop = () => stack[stack.length - 1];const ctx = {};const spush = (key, val) => stackTop()[key].push(val);const indentedTitle = ctxt =>`${stack.map(() => ' ').join('')}${ctxt}`;const newTop = title =>({ title, tests: [], setup: [], teardown: [], testSuites: [] });const execTop = () => 'setup tests testSuites teardown'.split(' ').forEach(key => stackTop()[key].forEach(activity[key]));const execTestSuite = (title, testSuiteFn) => {log(indentedTitle(title));stack.push(newTop(title));testSuiteFn.call(ctx); // collect testSuites, setup, teardown and it.execTop(); // execute themstack.pop();};const reportTests = (fn, title) => {const desc = indentedTitle(title);try {fn.call(ctx);success(desc);} catch (e) {failure(desc, e.message);}};activity.setup = fn => fn.call(ctx);activity.teardown = fn => fn.call(ctx);activity.testSuites = ([title, testFn]) =>execTestSuite(title, testFn);activity.tests = ([title, testFn]) =>reportTests(testFn, title);export const test = (desc, fn) => spush('tests', [desc, fn]);export const testSuite = (title, testfn) => {if (isEmptyStack()) {execTestSuite(title, testfn);return;}spush('testSuites', [title, testfn]);};export const setup = spush.bind(null, 'setup');export const teardown = spush.bind(null, 'teardown');
Its nice to find out that an expressive DSL could be implemented (to atleast experiment) in less than 100 lines of code.