doubleshot

Run separated BDD outlines and content on top of mocha

doubleshot

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).

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

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

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);
  });
});

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 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');
  });
});

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');
  }
}

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);
  });
});

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': {
    beforefunction () {
      // Runs before *all* contexts and assertions 
      this.banana = new Banana();
    },
    beforeEachfunction () {
      // Runs before *each* context and assertion 
    },
    afterEachfunction () {
      // Runs after *each* context and assertion 
    },
    afterfunction () {
      // Runs after *all* contexts and assertions 
    }
  }
}

For global hooks, define them directly on the content.

{
  beforefunction () {
    // Runs before *all* batches 
  },
  beforeEachfunction () {
    // Runs before *each* batch 
  },
  afterEachfunction () {
    // Runs after *each* batch 
  },
  afterfunction () {
    // Runs after *all* batches 
  },
  'A banana': {
    this.banana = new Banana();
  }
}

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

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.

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 */ }]`)

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;
  }
}

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);
  });
});

Support this project and others by twolfson via gittip.

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.

Copyright (c) 2013 Todd Wolfson

Licensed under the MIT license.