doubleshot

Run separated BDD outlines and content on top of mocha

npm install doubleshot
30 downloads in the last day
107 downloads in the last week
181 downloads in the last month

doubleshot Build status

Run separated BDD outlines and content on top of mocha.

doubleshot serves the single purpose of separating BDD outlines from their implementation. The benefits I will highlight are chaining and re-use of tests.

There are additional benefits such as re-using outlines between different frameworks however an ecosystem is missing for that.

This concept has been birthed out of my previous attempts to achieve a cross-framework testing solution (i.e. sculptor and crossbones).

Getting Started

doubleshot is a global module and is run on the command line

npm install -g doubleshot # Installs doubleshot globally
doubleshot # Runs content/outline files in test folder

// test/outline.yaml
One:
  - is equal to one

// test/content.js
{
  'One': function () {
    this.one = 1;
  },
  'is equal to one': function () {
    assert.strictEqual(this.one, 1);
  }
}

// Runs test as
describe('One', function () {
  before(function () {
    this.one = 1;
  });

  it('is equal to one', function () {
    assert.strictEqual(this.one, 1);
  });
});

// Output looks like
$ doubleshot --reporter spec

  One
    ✓ is equal to one

Documentation

doubleshot is run via the command line. By default, it attempts to find content and outline files in the test directory. As long as the file name ends with either outline or content, they will be found.

// Common outline filenames
test/outline.yaml
test/my_lib_outline.js
test/basic_tests_outline.json

// Common content filenames
test/content.js
test/my_lib_content.js
test/basic_tests_outline.js

Alternatively, you can specify a minimatch pattern to find them via --outline and --content options on the command line. By default, the minimatch patterns are:

test/*outline.{js,json,yaml} OR test/outline/{{name}}.{js,json,yaml}
test/*content.js OR test/content/{{name}}.js

Outline format

Outlines can be either in js, JSON, or YAML. For simplicity's sake, the majority of examples will use YAML.

Any new context (i.e. describe in mocha) is an object followed by an array. This array can either contain assertions or more contexts. The array is required for ensure that actions are run in order.

Any assertion (i.e. it in mocha) is a string.

The following outline:

# 'A banana' performs a `describe`
A banana:
  # 'is yellow' and 'has a peel' perform `it`s
  - is yellow
  - has a peel
  - when peeled:
    - is white
    - is soft

compiles to:

describe('A banana', function () {
  it('is yellow', contentGoesHere);
  it('has a peel', contentGoesHere);

  describe('when peeled', function () {
    it('is white', contentGoesHere);
    it('is soft', contentGoesHere);
  });
});

Using JSON for outlines

Initially, doubleshot was developed with inspiration from vows. However, the JSON format was unordered we required to move to an ordered JSON format. This was taken from YAML.

The format of the JSON is the same as the YAML; each context is a new object containing an array of either more contexts or assertions.

{
  // 'A banana' performs a `describe`
  'A banana': [
    // 'is yellow' and 'has a peel' perform `it`s
    'is yellow',
    'has a peel',
    {
      'when peeled': [
        'is white',
        'is soft'
      ]
    }
  ]
}

Content format

Content does not distinguish context from assertion since in mocha they have the same signature. To aim for the simplest format possible, we collect them in a large object.

The keys represent the name of the context/assertion you used in the outline.

If the key is for a context, the function will be run in a before block.

If the key is for an assertion, the function will be run in an it block.

The following code:

{
  'A banana': function () {
    var banana = new Banana();
  },
  'is yellow': function () {
    assert.strictEqual(banana.color, 'yellow');
  }
}

compiles to:

describe('A banana', function () {
  before(function () {
    var banana = new Banana();
  });

  it('is yellow', function () {
    assert.strictEqual(banana.color, 'yellow');
  });
});

Context; using this

mocha allows for the usage of this as a shared store between all contexts and assertions.

This means, if you define a property on this in a before block, you can read it in the it block.

In doubleshot, this is strongly encouraged to prevent global namespace pollution (i.e. writing to window or global to share variables) or scope leaks (i.e. writing to a var outside of the current function).

Here is a usage in mocha:

describe('A banana', function () {
  before(function () {
    // Save this banana to the test context
    this.banana = new Banana();
  });

  it('is yellow', function () {
    // Retrieve the banana from the test context
    var banana = this.banana;

    // Test against the banana
    assert.strictEqual(banana.color, 'yellow');
  });
});

Here is that same usage in doubleshot:

{
  'A banana': function () {
    // Save this banana to the test context
    this.banana = new Banana();
  },
  'is yellow': function () {
    // Retrieve the banana from the test context
    var banana = this.banana;

    // Test against the banana
    assert.strictEqual(banana.color, 'yellow');
  }
}

Combining outline and content

The combination process in doubleshot is string matching. If a key in outline matches a key in content, then the value from content is assigned to that from outline.

If you have any keys in one set that are not matched to another, doubleshot will let you know via a console.error message.

When we combine this outline:

A banana:
  - is yellow
  - has a peel

and this content:

{
  'A banana': function () {
    this.banana = new Banana();
  },
  'is yellow': function () {
    assert.strictEqual(this.banana.color, 'yellow');
  }
  'has a peel': function () {
    assert(this.banana.peel);
  }
}

They match along the A banana, is yellow, and has a peel keys, returning:

describe('A banana', function () {
  before(function () {
    this.banana = new Banana();
  });

  it('is yellow', function () {
    assert.strictEqual(this.banana.color, 'yellow');
  });

  it('has a peel', function () {
    assert(this.banana.peel);
  });
});

Hooks

The normal mocha hooks (e.g. after, beforeEach, afterEach) are available on the global level and per-context level.

If you provide an object instead of a function, you can define the hooks individually by name.

{
  'A banana': {
    before: function () {
      // Runs before *all* contexts and assertions
      this.banana = new Banana();
    },
    beforeEach: function () {
      // Runs before *each* context and assertion
    },
    afterEach: function () {
      // Runs after *each* context and assertion
    },
    after: function () {
      // Runs after *all* contexts and assertions
    }
  }
}

For global hooks, define them directly on the content.

{
  before: function () {
    // Runs before *all* batches
  },
  beforeEach: function () {
    // Runs before *each* batch
  },
  afterEach: function () {
    // Runs after *each* batch
  },
  after: function () {
    // Runs after *all* batches
  },
  'A banana': {
    this.banana = new Banana();
  }
}

Aliasing and expansion

One of the bonus features of doubleshot is aliasing and expansion.

Aliasing

Any key you define inside of content can be the name of another content property (e.g. "1" can point to "One").

If an alias is not found, you will be notified via a console.error message.

Expansion

Any key you define inside of content can be an array of names of other content properties (e.g. "1 + 2" can point to "One" and "plus two", which are run in order).

You can also define a function inline in your expansion (e.g. "1 + 2" ->["One", function () { / plus two / }]`)

Unlimited depth and chaining

You can infinitely chain aliases and expansions (e.g. "1 + 2" -> ["1", "+ 2"] -> ["One", "plus two"] -> ["Zero", "plus one", "plus one", "plus one"]). In code, that would look like:

// content.js
{
  "1 + 2": ["1", "+ 2"],
  "1": "One",
  "One": ["Zero", "plus one"],
  "+ 2": "plus two",
  "plus two": ["plus one", "plus one"],
  "Zero": function () {
    this.sum = 0;
  },
  "plus one": function () {
    this.sum = 1;
  }
}

Examples

Here is a full example of using expansion and aliasing:

// outline.yaml
'1 + 2': ['= 3']

// content.js
{
  // Breaks 'One plus two' action into 2 actions
  '1 + 2': ['One', 'plus two'],
  'One': function () {
    this.sum = 1;
  },
  'plus two': function () {
    this.sum += 2;
  },
  // Alias 'is equal to three' as 'equals three'
  '= 3': 'equals three',
  'equals three': function () {
    assert.strictEqual(this.sum, 3);
  }
}

// Runs test as
describe('1 + 2', function () {
  before(function () {
    // These are contained inside functions but have the same effect
    this.sum = 1;
    this.sum += 2;
  });

  it('= 3', function () {
    assert.strictEqual(this.sum, 3);
  });
});

Donating

Support this project and others by twolfson via gittip.

Support via Gittip

Contributing

In lieu of a formal styleguide, take care to maintain the existing coding style. Add unit tests for any new or changed functionality. Lint your code using grunt and test via npm test.

License

Copyright (c) 2013 Todd Wolfson

Licensed under the MIT license.

npm loves you