75

Problem

I have multiple Mocha tests that perform the same actions, leading to code duplication. This impacts the maintainability of the code base.

var exerciseIsPetitionActive = function (expected, dateNow) {
    var actual = sut.isPetitionActive(dateNow);
    chai.assert.equal(expected, actual);
};

test('test_isPetitionActive_calledWithDateUnderNumSeconds_returnTrue', function () {
    exerciseIsPetitionActive(true, new Date('2013-05-21 13:11:34'));
});

test('test_isPetitionActive_calledWithDateGreaterThanNumSeconds_returnFalse', function () {
    exerciseIsPetitionActive(false, new Date('2013-05-21 13:12:35'));
});

What I'm Looking For

I'm seeking a method to consolidate my duplicated Mocha tests into a single one.

As an example, PHPUnit (and other testing frameworks) incorporate data providers. In PHPUnit, a data provider operates as follows:

<?php class DataTest extends PHPUnit_Framework_TestCase {
    /**
     * @dataProvider provider
     */
    public function testAdd($a, $b, $c)
    {
        $this->assertEquals($c, $a + $b);
    }

    public function provider()
    {
        return array(
          array(0, 0, 0),
          array(0, 1, 1),
          array(1, 0, 1),
          array(1, 1, 3)
        );
    }
}

The data provider in this case injects parameters into the test, allowing the test to execute all cases efficiently - ideal for handling duplicated tests.

I'm curious to learn if Mocha has a similar feature or functionality, such as:

var exerciseIsPetitionActive = function (expected, dateNow) {
    var actual = sut.isPetitionActive(dateNow);
    chai.assert.equal(expected, actual);
};

@usesDataProvider myDataProvider
test('test_isPetitionActive_calledWithParams_returnCorrectAnswer', function (expected, date) {
    exerciseIsPetitionActive(expected, date);
});

var myDataProvider = function() {
  return {
      {true, new Date(..)},
      {false, new Date(...)}
  };
};

What I've Explored

I've come across a technique called Shared Behaviors. However, it doesn't directly address the issue within a test suite; it only handles duplicated tests across different components.

The Question

Does anyone know how to implement data providers in Mocha?

Tomás
  • 3,501
  • 3
  • 21
  • 38

6 Answers6

42

A basic approach to run the same test with different data is to repeat the test in a loop providing the data:

describe('my tests', function () {
  var runs = [
    {it: 'options1', options: {...}},
    {it: 'options2', options: {...}},
  ];

  before(function () {
    ...
  });

  runs.forEach(function (run) {
    it('does sth with ' + run.it, function () {
      ...
    });
  });
});

before runs, well, before all its in a describe. If you need to use some of the options in before, do not include it in the forEach loop because mocha will first run all befores and the all its, which is probably not wanted. You can either put the whole describe in the loop:

var runs = [
  {it: 'options1', options: {...}},
  {it: 'options2', options: {...}},
];

runs.forEach(function (run) {
  describe('my tests with ' + run.it, function () {
    before(function () {
      ...
    });

    it('does sth with ' + run.it, function () {
      ...
    });
  });
});

If you do not wish to pollute your tests with multiple describes, you can use the controversial module sinon for this matter:

var sinon = require('sinon');

describe('my tests', function () {
  var runs = [
    {it: 'options1', options: {...}},
    {it: 'options2', options: {...}},
  ];

  // use a stub to return the proper configuration in `beforeEach`
  // otherwise `before` is called all times before all `it` calls
  var stub = sinon.stub();
  runs.forEach(function (run, idx) {
    stub.onCall(idx).returns(run);
  });

  beforeEach(function () {
    var run = stub();
    // do something with the particular `run.options`
  });

  runs.forEach(function (run, idx) {
    it('does sth with ' + run.it, function () {
      sinon.assert.callCount(stub, idx + 1);
      ...
    });
  });
});

Sinon feels dirty but is effective. Several aid modules such as leche are based on sinon, but arguably introducing further complexity is not necessary.

Wtower
  • 18,848
  • 11
  • 103
  • 80
  • My linter red lines the foreach call unless I use "// tslint:disable-next-line:mocha-no-side-effect-code" – cmzmuffin Oct 28 '21 at 12:07
37

Mocha doesn't provide a tool for that, but it is easy to do it yourself. You only need to run the tests inside a loop and give the data to the test function using a closure:

suite("my test suite", function () {
    var data = ["foo", "bar", "buzz"];
    var testWithData = function (dataItem) {
        return function () {
            console.log(dataItem);
            //Here do your test.
        };
    };

    data.forEach(function (dataItem) {
        test("data_provider test", testWithData(dataItem));
    });
});
Tomás
  • 3,501
  • 3
  • 21
  • 38
Kaizo
  • 4,155
  • 2
  • 23
  • 26
  • 6
    This is not working for me. I'm using the ```describe()```, ```it()``` syntaxe. Is it important to return a test function or could we do the test directly in ```testWithData(dataItem)``` ? – svassr Aug 22 '14 at 13:22
  • 3
    I had to make sure every extra javascript (like ```for each```) is inside a ```it('tests', function(){ /*HERE */ })```. As I use ```it()```for each test inside the loop I had to wrap everything inside a ```describe(...)```. I ended up with a hierarchye ```describe > it > forEach > describe > it```, which make all the tests in my loop appear at the end of my tests, not matter when it occurs during test, but it works. – svassr Aug 22 '14 at 15:41
  • can you provide a gist with `it()` and `describe()`? – chovy Apr 13 '15 at 05:48
  • @Kaizo: this will help only for a single it, assumming `describe --> it --> forEach --> run asserts here with different values based on forEach loop`, But how about running complete `describe` multiple time with different value for a single variable ? otherwise its again duplicate `forEach` inside each `it` – lucky Dec 07 '15 at 09:11
  • 1
    My apologies if this is not the correct forum for asking this question, but this doesn't seem work for a few reasons: - Mocha doesn't seem to have "suite" or "test(foo)" constructs - Using this *exact* code doesn't work - it errors out with "suite is not defined". How is this being claimed to be the answer? Versions: Node 6.6.0, Mocha 3.1.2 – Rubicon Dec 08 '16 at 17:14
  • 1
    @Rubicon you need to run the tests with the "tdd" interface. To use the "bdd" just change "suite" for "describe" and "test" for "it". You can read more about it here: https://mochajs.org/#interfaces – Kaizo Dec 20 '16 at 09:31
5

Leche adds that functionality to Mocha. See the announcement and docs.

It is better than simply looping over the tests because, if a test fails, it tells you which data set was involved.

Update:

I didn't like the setup of Leche and haven't managed to get it to work with Karma, so eventually I have extracted the data provider into a separate file.

If you want to use it, just grab the source. Documentation is available in the Leche readme, and you'll find additional info and usage tips in the file itself.

hashchange
  • 7,029
  • 1
  • 45
  • 41
2

Based on the @Kaizo's answer, here's what I came up with for my test (it's a controller that is getting some parameters from the request) to emulate the data provider in PHPUnit. The getParameters method is going to receive the request from Express, and then use req.param to inspect some query parameters, for example, GET /jobs/?page=1&per_page=5. This also shows how to stub the Express request object.

Hopefully it can help someone as well.

// Core modules.
var assert = require('assert');

// Public modules.
var express = require('express');
var sinon = require('sinon');

// Local modules.
var GetJobs = require(__base + '/resources/jobs/controllers/GetJobs');

/**
 * Test suite for the `GetJobs` controller class.
 */
module.exports = {
    'GetJobs.getParameters': {
        'should parse request parameters for various cases': function () {
            // Need to stub the request `param` method; see http://expressjs.com/3x/api.html#req.param
            var stub = sinon.stub(express.request, 'param');
            var seeds = [
                // Expected, page, perPage
                [{limit: 10, skip: 0}],
                [{limit: 5, skip: 10}, 3, 5]
            ];
            var controller = new GetJobs();

            var test = function (expected, page, perPage) {
                stub.withArgs('page').returns(page);
                stub.withArgs('per_page').returns(perPage);

                assert.deepEqual(controller.getParameters(express.request), expected);
            };

            seeds.forEach(function (seed) {
                test.apply({}, seed);
            });
        }
    }
};

The only downside is Mocha doesn't count the actual assertions (like PHPUnit does), it just shows up as one test.

Andrew Eddie
  • 988
  • 6
  • 15
1

A simpler solution is described below using mocha-testdata library.

Sample solution to the problem.

import * as assert from assert;
import { givenAsync } from mocha-testdata;

suite('My async test suite', function () {
  given([0, 0, 0], [0, 1, 1], [1, 0, 1], [1, 1, 3]).test('sum to 6', function (a, b, c) {
    assert.strictEqual(a + b + c, 6);
  });
});

If you need to test async function calls which is the most common case in node.js app, use givenAsync instead.

import * as assert from assert;
import { givenAsync } from mocha-testdata;

suite('My async test suite', function () {
  givenAsync([1, 2, 3], [3, 2, 1]).test('sum to 6', function (done, a, b, c) {
    doSomethingAsync(function () {
        assert.strictEqual(a + b + c, 6);
        done();
    });
  });
});
0

I've found mocha-testcheck to be the easiest tool for this. It generates all kinds of data. It will narrow down which input is causing your test to fail.

Zachary Ryan Smith
  • 2,688
  • 1
  • 20
  • 30