mocha-given
v0.4.0
Published
Adds a Given-When-Then DSL to mocha as an alternative style for specs
Maintainers
Readme
mocha-given
Mocha-given is a mocha interface that helps you write cleaner specs using Given, When, Then and And.
It is a shameless port of Justin Searls' jasmine-given which is a tribute to Jim Weirich's terrific rspec-given gem.
Zero dependencies. Works with mocha 8 through 11.
Install
$ npm install --save-dev mocha mocha-givenRun your specs with the interface set to mocha-given:
$ mocha --ui mocha-givenExample specs
JavaScript
describe('assigning stuff to this', function () {
Given(function () { this.number = 24; });
When(function () { this.number++; });
And(function () { this.number *= 2; });
Then(function () { return this.number === 50; });
});
describe('assigning stuff to variables', function () {
let subject = null;
Given(() => { subject = []; });
When(() => { subject.push('foo'); });
Then(() => subject.length === 1);
});
describe('Testing deferred', function () {
Given(function () { this.t = Date.now(); });
Then.after(1500, 'so much time has passed', function () {
return Date.now() - this.t >= 1500;
});
});
describe('Testing async', function () {
Given(function () { this.subject = new User(); });
Then('save user', function (done) { this.subject.save(done); });
});CoffeeScript
describe 'assigning stuff to this', ->
Given -> @number = 24
When -> @number++
And -> @number *= 2
Then -> @number == 50
describe 'assigning stuff to variables', ->
subject = null
Given -> subject = []
When -> subject.push('foo')
Then -> subject.length == 1
describe 'Testing deferred', ->
Given -> @t = Date.now()
Then.after 1500, 'so much time has passed', -> Date.now() - @t >= 1500
describe 'Testing async', ->
Given -> @subject = new User()
Then 'save user', (done) -> @subject.save(done);CoffeeScript specs need the compiler registered yourself, which mocha-given no longer does for you:
$ mocha --ui mocha-given --require coffeescript/register --extension coffeeArrow functions and this
Both styles below work, but they are not interchangeable.
State shared between Given, When and Then lives on mocha's test context,
which you reach through this. Arrow functions do not bind this, and neither
call nor apply can change that, so a spec that touches this must use a
classic function:
Given(function () { this.number = 24; }); // writes to the test context
Then(function () { return this.number === 24; });
Given(() => { this.number = 24; }); // writes to module scope
Then(() => this.number === 24); // reads module scopeThe arrow version is worse than it looks. It often passes, because in
CommonJS the module-level this is module.exports, so both arrows share one
object. But that object is global to the file and is never reset between tests,
so state leaks from one spec into the next:
describe('first', function () {
Given(() => { this.n = 1; });
Then(() => this.n === 1); // passes
});
describe('second', function () {
Then(() => this.n === 1); // also passes, with no Given at all
});Specs built on closure variables have no such constraint, and arrows read better there:
let subject = null;
Given(() => { subject = []; });
Then(() => subject.length === 0);| Spec style | Use |
| --- | --- |
| this.foo shared state | function |
| closure variables | arrow, or function |
And after Then
An And following a Then becomes part of that same spec, so the Given and
When setup runs once for the whole group rather than once per assertion:
Given(function () { this.subject = expensiveSetup(); });
When(function () { this.result = this.subject.run(); });
Then(function () { return this.result.ok === true; });
And(function () { return this.result.items.length === 3; });
And(function () { return this.result.errors.length === 0; });That is one test, one setup. Its title joins the assertions:
✔ then this.result.ok === true and this.result.items.length === 3 and this.result.errors.length === 0Two separate Thens still run the setup twice, which is the distinction
rspec-given and
jasmine-given draw. Use And when
several assertions describe one outcome, and a second Then when you want a
fresh fixture.
And after a Given, When or Invariant is unchanged: it repeats that
construct.
Promises
Given, When, Invariant and Then may all return a promise, and each step
is awaited before the next one runs.
describe('loading a user', function () {
Given('user', async () => fetchUser(1));
When('name', function () { return this.user.name; });
Then(function () { return this.name === 'Ada'; });
});The named forms assign the resolved value, not the promise, so this.user
above is the user object.
A Then that resolves to false fails, and a rejection fails with its own
error. A step may take a done callback or return a promise, but not both.
API
| | |
| --- | --- |
| Given(fn) | Runs before each Then in scope. Sets up state |
| Given(name, fn) | Assigns the return value to this[name] |
| When(fn) | Runs after all Givens, immediately before each Then |
| When(name, fn) | Assigns the return value to this[name] |
| Then(fn) | A spec. Fails if fn returns false or throws |
| Then(label, fn) | Same, with an explicit title |
| Then.after(ms, label, fn) | Runs the assertion after a delay |
| Then.only(...) | Runs only this spec |
| And(fn) | Repeats whichever of Given, When or Invariant came last. After a Then it adds an assertion to that same spec |
| Invariant(fn) | Asserted before every Then in scope |
A Then without a label takes its title from the source of the expression, so
Then(() => this.sum === 5) reads as then this.sum === 5. Comments inside the
function body end up in the title, so keep them above the call.
When an assertion fails, the comparison is reported with both sides evaluated:
1) then this.sum === 99:
Error: return value is false
Expected '5' to strictly equal '99'
Comparison: this.sum === 99Run tests programmatically
const Mocha = require('mocha');
const fs = require('fs');
const path = require('path');
// require mocha-given after Mocha is loaded
require('mocha-given');
const testDir = 'test';
const mocha = new Mocha({
ui: 'mocha-given',
reporter: 'spec',
});
fs.readdirSync(testDir)
.filter((file) => /\.(coffee|js)$/.test(file))
.forEach((file) => mocha.addFile(path.join(testDir, file)));
mocha.run((failures) => {
process.exitCode = failures ? 1 : 0;
});Contributing
$ npm install
$ npm testnpm test runs the JavaScript specs and then a small CoffeeScript spec that
guards against breaking CoffeeScript users.
Credits
Thanks to SinnerSchrader for their support and the time to work on this project.
License
MIT
