Skip to content
This repository has been archived by the owner on Apr 15, 2019. It is now read-only.

Commit

Permalink
Merge pull request #344 from LiskHQ/321-mocha_gwt_wrappers
Browse files Browse the repository at this point in the history
Add Mocha Given/When/Then wrappers - Closes #321
  • Loading branch information
willclarktech committed Nov 15, 2017
2 parents 1fc3138 + f4a64df commit 7c04aff
Show file tree
Hide file tree
Showing 29 changed files with 1,488 additions and 2,199 deletions.
6 changes: 5 additions & 1 deletion test/.eslintrc.json
Expand Up @@ -3,12 +3,16 @@
"mocha": true
},
"globals": {
"Given": true,
"When": true,
"Then": true,
"sandbox": true,
"should": true,
"sinon": true
},
"rules": {
"arrow-body-style": "off",
"max-len": "off"
"max-len": "off",
"new-cap": ["error", { "capIsNewExceptions": ["Given", "When", "Then"] }]
}
}
38 changes: 25 additions & 13 deletions test/README.md
Expand Up @@ -16,22 +16,19 @@ This document provides guidelines for contributing such tests.
A specification consists of a suite of nested steps. Specifications should be written using language which is neutral with regard to test implementation. Here’s an abridged example from `test/specs/utils/cryptoModule.js`:

```js
import { setUpCommandCreateAccount } from '../../steps/setup';
import * as given from '../../steps/1_given';
import * as when from '../../steps/2_when';
import * as then from '../../steps/3_then';

describe('create account command', () => {
describe('Given a crypto instance', () => {
beforeEach(given.aCryptoInstance);
describe('Given a passphrase "minute omit local rare sword knee banner pair rib museum shadow juice" with private key "314852d7afb0d4c283692fef8a2cb40e30c7a5df2ed79994178c10ac168d6d977ef45cd525e95b7a86244bbd4eb4550914ad06301013958f4dd64d32ef7bc588" and public key "7ef45cd525e95b7a86244bbd4eb4550914ad06301013958f4dd64d32ef7bc588" and address "2167422481642255385L"', () => {
beforeEach(given.aPassphraseWithPrivateKeyAndPublicKeyAndAddress);
describe('Given the passphrase is generated by the createMnemonicPassphrase function', () => {
beforeEach(given.thePassphraseIsGeneratedByTheCreateMnemonicPassphraseFunction);
describe('Given an action "create account"', () => {
beforeEach(given.anAction);
describe('When the action is called', () => {
beforeEach(when.theActionIsCalled);
it('Then it should resolve to an object with the passphrase and the publicKey and the address', then.itShouldResolveToAnObjectWithThePassphraseAndThePublicKeyAndTheAddress);
beforeEach(setUpCommandCreateAccount);
Given('a crypto instance', given.aCryptoInstance, () => {
Given('a passphrase "minute omit local rare sword knee banner pair rib museum shadow juice" with private key "314852d7afb0d4c283692fef8a2cb40e30c7a5df2ed79994178c10ac168d6d977ef45cd525e95b7a86244bbd4eb4550914ad06301013958f4dd64d32ef7bc588" and public key "7ef45cd525e95b7a86244bbd4eb4550914ad06301013958f4dd64d32ef7bc588" and address "2167422481642255385L"', given.aPassphraseWithPrivateKeyAndPublicKeyAndAddress, () => {
Given('the passphrase is generated by the createMnemonicPassphrase function', given.thePassphraseIsGeneratedByTheCreateMnemonicPassphraseFunction, () => {
Given('an action "create account"', given.anAction, () => {
When('the action is called', when.theActionIsCalled, () => {
Then('it should resolve to an object with the passphrase and the publicKey and the address', then.itShouldResolveToAnObjectWithThePassphraseAndThePublicKeyAndTheAddress);
});
});
});
Expand All @@ -46,14 +43,23 @@ For anyone who’s used Gherkin syntax (e.g. for end-to-end testing of web appli
1. **When**: for executing the code under test
1. **Then**: for making assertions against the result

The `Given` and `When` functions are just wrapped versions of Mocha’s `describe`: the description is prepended with the corresponding prefix, and the function passed immediately after the description is passed to the `beforeEach` hook for that suite. `Then` is just Mocha’s `it` with a prefixed description. These functions are globals when running tests, so you don’t need to worry about importing them.

As you can see, there is no indication here as to how the test code should be written, and the test descriptions are written in ordinary English as far as possible (bearing in mind that this is a specification of a relatively low-level function so some degree of technical language is inevitable).

In contrast to Gherkin and standard end-to-end tests, suites in Mocha can be easily nested, so appropriate organisation of your specification files can reduce verbosity and highlight relationships between the various components being specified.

The test descriptions should specify examples of any variables (e.g. `Then the crypto instance should have name "Crypto"`) that may be relevant so that anyone reading the specification can see what a realistic example is and can easily tell when the example diverges from real-life requirements. A corresponding step definition is then passed via a `beforeEach` hook in the case of a suite, or directly to the test in the case of an `it` call. The step definition function name should match the description exactly, but with the specific details removed. `beforeEach` is used to ensure the tests are atomic and isolated from each other.
The test descriptions should specify examples of any variables (e.g. `the crypto instance should have name "Crypto"`) that may be relevant so that anyone reading the specification can see what a realistic example is and can easily tell when the example diverges from real-life requirements. A corresponding step definition is then passed via a `beforeEach` hook in the case of a suite, or directly to the test in the case of a `Then` step. The step definition function name should match the description exactly, but with the specific details removed. `beforeEach` is used to ensure the tests are atomic and isolated from each other.

Notice also the use of ES6 arrow functions in specification files.

## Set-up

If your tests need any kind of set-up, it should have its own dedicated set-up function passed to the `beforeEach` hook of the outer `describe` block. Examples of things to include in such a function:

- Stubbing dependencies (but not necessarily specifying what those stubs return).
- Storing environmental variables so they can be reset after the tests.

## Step definitions

A step definition converts a step from a specification into a concrete implementation that will enforce the specification. Here are some abridged examples relating to the above specification:
Expand Down Expand Up @@ -82,7 +88,9 @@ export function anAction() {
const actionName = getFirstQuotedString(this.test.parent.title);
this.test.ctx.action = getActionCreator(actionName)();
}
```

```js
// test/steps/.../2_when.js

export function theActionIsCalled() {
Expand All @@ -91,7 +99,9 @@ export function theActionIsCalled() {
this.test.ctx.returnValue = returnValue;
return returnValue.catch(e => e);
}
```

```js
// test/steps/.../3_then.js

export function itShouldResolveToAnObjectWithThePassphraseAndThePublicKeyAndTheAddress() {
Expand All @@ -114,7 +124,9 @@ Here the **Given** steps are responsible for setup (storing values in the test c

## Benefits

In brief:
More information is available in [this blog post][blog-post], but in brief:

1. Separating specification from test implementation enables clearer thinking about desired source code behaviour.
1. Atomic step definitions results in DRYer, more reusable, more manageable test code.

[blog-post]: https://blog.lisk.io/bdd-style-unit-testing-with-mocha-704137e429d5
11 changes: 11 additions & 0 deletions test/setup.js
Expand Up @@ -35,3 +35,14 @@ should.use((_, Assertion) => {
Object.defineProperty(global, 'should', { value: should });
global.sinon = sinon;
global.sandbox = sinon.sandbox.create();

const createPreStep = prefix => (description, beforeEachHook, suiteBody) => {
describe(`${prefix} ${description}`, () => {
beforeEach(beforeEachHook);
suiteBody();
});
};

global.Given = createPreStep('Given');
global.When = createPreStep('When');
global.Then = (description, testBody) => it(`Then ${description}`, testBody);
17 changes: 6 additions & 11 deletions test/specs/commands/createAccount.js
Expand Up @@ -20,17 +20,12 @@ import * as then from '../../steps/3_then';

describe('create account command', () => {
beforeEach(setUpCommandCreateAccount);
describe('Given a crypto instance has been initialised', () => {
beforeEach(given.aCryptoInstanceHasBeenInitialised);
describe('Given a passphrase "minute omit local rare sword knee banner pair rib museum shadow juice" with private key "314852d7afb0d4c283692fef8a2cb40e30c7a5df2ed79994178c10ac168d6d977ef45cd525e95b7a86244bbd4eb4550914ad06301013958f4dd64d32ef7bc588" and public key "7ef45cd525e95b7a86244bbd4eb4550914ad06301013958f4dd64d32ef7bc588" and address "2167422481642255385L"', () => {
beforeEach(given.aPassphraseWithPrivateKeyAndPublicKeyAndAddress);
describe('Given the passphrase is generated by the createMnemonicPassphrase function', () => {
beforeEach(given.thePassphraseIsGeneratedByTheCreateMnemonicPassphraseFunction);
describe('Given an action "create account"', () => {
beforeEach(given.anAction);
describe('When the action is called', () => {
beforeEach(when.theActionIsCalled);
it('Then it should resolve to an object with the passphrase and the publicKey and the address', then.itShouldResolveToAnObjectWithThePassphraseAndThePublicKeyAndTheAddress);
Given('a crypto instance has been initialised', given.aCryptoInstanceHasBeenInitialised, () => {
Given('a passphrase "minute omit local rare sword knee banner pair rib museum shadow juice" with private key "314852d7afb0d4c283692fef8a2cb40e30c7a5df2ed79994178c10ac168d6d977ef45cd525e95b7a86244bbd4eb4550914ad06301013958f4dd64d32ef7bc588" and public key "7ef45cd525e95b7a86244bbd4eb4550914ad06301013958f4dd64d32ef7bc588" and address "2167422481642255385L"', given.aPassphraseWithPrivateKeyAndPublicKeyAndAddress, () => {
Given('the passphrase is generated by the createMnemonicPassphrase function', given.thePassphraseIsGeneratedByTheCreateMnemonicPassphraseFunction, () => {
Given('an action "create account"', given.anAction, () => {
When('the action is called', when.theActionIsCalled, () => {
Then('it should resolve to an object with the passphrase and the publicKey and the address', then.itShouldResolveToAnObjectWithThePassphraseAndThePublicKeyAndTheAddress);
});
});
});
Expand Down

0 comments on commit 7c04aff

Please sign in to comment.