Skip to content

pghalliday/forgiven

Repository files navigation

Forgiven

Build Status Coverage Status

Extensible given/when/then and more for test frameworks

Usage

install forgiven and a UI factory (forgiven-mocha in this case) as a development dependency.

npm install --save-dev mocha forgiven forgiven-mocha

Then initialize a new given function, with the UI factory

import {
  create,
} from 'forgiven';
import {
  mocha,
} from 'forgiven-mocha';

global.given = create(mocha);

The given function can then be used to create concise test chains in place of the verbose describe/beforeEach/it/afterEach format.

import {
  greet,
} from '../src/greet';

let greeting;
let person;

describe('greet', () => {
  given(() => greeting = 'hello')
  .and(() => person = 'fred')
  .then(() => greet(greeting, person).should.eql('hello fred'))
  .end();
});

Note that the chain can consist of multiple setup steps followed by multiple test steps. Test steps start after the first then after which only test steps are allowed.

The chain must be ended with a call to end which will actually render the chain so far (as an aside, multiple calls to end will render the tests multiple times... don't bother doing that).

Chains can also be reused to keep your tests DRY, but rememeber to only end them once after completely defining them.

const chain = given(() => greeting = 'hello')
.and(() => person = 'fred');

chain
.then(() => greet(greeting, person).should.eql('hello fred'));

chain
.or.with(() => greeting = 'bonjour')
.then(() => greet(greeting, person).should.eql('bonjour fred'));

chain
.or.with(() => greeting = 'ola')
.then(() => greet(greeting, person).should.eql('ola fred'));

chain.end();

You may prefer to use the fork method for reusing chains in order to layout your shared chains more clearly.

given(() => greeting = 'hello')
.and(() => person = 'fred')
.fork((chain) => {
  chain
  .then(() => greet(greeting, person).should.eql('hello fred'));

  chain
  .or.with(() => greeting = 'bonjour')
  .then(() => greet(greeting, person).should.eql('bonjour fred'));

  chain
  .or.with(() => greeting = 'ola')
  .then(() => greet(greeting, person).should.eql('ola fred'));
})
.end();

Another bonus to using fork is that if you attempt to end a chain inside a fork then an error will be thrown.

So we have only seen the most concise form of setup and test definition, however the full function signatures for setup steps are

setup(description, beforeEach, afterEach);
setup(beforeEach, afterEach);
setup(beforeEach);
setup({description, beforeEach, afterEach});

When no description is given then the body of the beforeEach callback will be used as the description.

The full function signatures for test steps are

test(description, test);
test(test);
test({description, test});

When no description is given then the body of the test callback will be used as the description.

You will have noticed that various grammatical constructions can be made. There are in fact a fixed list of words that can be used for setup steps and another list for test steps. The choice of wording only affects the generated test reports as they are prepended to the descriptions. The words can also be chained indefinitely (even if it doesn't make sense) and they will all be prefixed to test and setup descriptions. The only limitation is that the setup chain must begin with given and the test phase must begin with then.

The valid words for setup steps are

export const SETUP_CONJUNCTIONS = [
  'given',
  'when',
  'where',
  'while',
  'with',
  'and',
  'or',
  'after',
  'once',
];

The valid words for test steps are

export const TEST_CONJUNCTIONS = [
  'then',
  'and',
];

Pending Setups and Tests

Any setup or test step can be marked as pending (skipped) using the following modifiers.

export const PENDING_MODIFIERS = [
  'skip',
  'pending',
  'eventually',
];

eg.

given(() => greeting = 'hello')
.and(() => person = 'fred')
.then.eventually(() => greet(greeting, person).should.eql('hello fred'))
.end();

It is then up to the UI factory to handle the pending flag if it supports it.

Exclusive Setups and Tests

Any setup or test step can be marked as exclusive (other tests will be skipped) using the following modifiers.

export const ONLY_MODIFIERS = [
  'only',
  'just',
  'exclusive',
];

eg.

given(() => greeting = 'hello')
.and(() => person = 'fred')
.then.just(() => greet(greeting, person).should.eql('hello fred'))
.end();

It is then up to the UI factory to handle the only flag if it supports it.

Plugins

Plugins can be defined and used to extend the setup steps so that complex behaviour can be expressed in a concise and natural manner. They are registered with the create method.

import {
  create,
} from 'forgiven';
import {
  mocha,
} from 'forgiven-mocha';
import {
  promise,
} from 'forgiven-promise';

global.given = create(mocha, {
  promise: promise,
});

The following plugins are currently available

Each plugin is registered with a name that can be used with setup steps using a determiner.

export const DETERMINERS = [
  'the',
  'a',
  'an',
];

For example

const context = {};

given.a.promise.as(context, 'promise').from(() => Promise.reject(new Error('FAIL')))
.then(() => context.promise.should.be.rejectedWith('FAIL'))
.end();

Creating a plugin is easy (well it depends how complicated you want to make it). A plugin is defined as a function that takes a setup step function as its only parameter. The function then returns an object or function to assign to the registered plugin name. It's important that something inside the plugin calls the setup function and eventually returns the return value so that the chain can be continued.

function myPlugin(setup) {
  return (params) => {
    return setup({
      description: params.description,
      beforeEach: params.beforeEach,
      afterEach: params.afterEach,
    });
  };
}

UI Factories

UI factories can be created to support various test frameworks depending on their features. Currently the following UI factories exist.

A UI factory is defined as a function that returns a function that calls the initial setup and handles setup and test callbacks. Any of the above factories provide an example but as a skeleton, the following should provide guidance.

// To define...
//
//  doSetupWithModifiers
//  doBeforeEach
//  doAfterEach
//  doTest
//

function setup({
  description,
  beforeEach,
  afterEach,
  pending,
  only,
}, callback) {
  // add the setup phase with modifiers
  // for pending and only if supported
  doSetupWithModifiers(description, pending, only, () => {
    // call the beforeEach/afterEach
    // functions if specified
    doBeforeEach(beforeEach);
    doAfterEach(afterEach);

    // insert child setups and tests
    callback(
      setup,
      test,
    );
  });
}

function test({description, pending, only, test}) {
  // add a test with modifiers if supported
  doTest(description, pending, only, test);
}

export function uiFactory(params, callback) {
  return () => {
    setup(params, callback);
  };
}

Contributing

Run tests and build before pushing/opening a pull request.

  • npm test - lint and test
  • npm start - watch and build, etc with alarmist
  • npm run build - run tests then build
  • npm run watch - watch for changes and run build
  • npm run ci - run build and submit coverage to coveralls

About

Extensible given/when/then and more for test frameworks

Resources

Stars

Watchers

Forks

Packages

No packages published