Mocha: Can't dynamically create tests

Created on 31 Dec 2014  路  7Comments  路  Source: mochajs/mocha

If I try to create tests in BDD style with it() dynamically at "runtime", Mocha ignores them. See below for an example (live viewable example at http://sil.jsbin.com/vuteko/edit?html,output).

Note that if I populate TEST_NAMES statically (hardcode the entries into it in the global scope) then it works fine, but attempting to add them in before() does not work, even though before runs before the tests themselves (that's why it's called "before", after all). It does not seem to run before the thing which gathers which tests there _are_, though. Is there any way around this?

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Mocha: dynamic tests don't work (hotlinking from mocha site)</title>
    <link rel="stylesheet" href="http://mochajs.org/example/mocha.css">
  </head>
  <body>
    <div id="mocha"></div>
    <script src="http://mochajs.org/example/mocha.js"></script>
    <script src="http://mochajs.org/example/chai.js"></script>
    <script>mocha.setup('bdd')</script>
    <script>expect = chai.expect</script>
    <script>
    var TEST_NAMES = [];
    describe('Mocha tests', function(){

      before(function(done) {
        TEST_NAMES = ["test1", "test2"];
        done();
      });

      describe('Static hardcoded tests', function(){
        it('should correctly add numbers', function(){
          expect(1+1).to.equal(2);
          expect(1+1).to.not.equal(3);
        });
      });

      // So why don't these dynamic tests run? We populate TEST_NAMES 
      // in before(), which clearly should run before the tests.
      describe('Dynamically created tests (which do not work)', function() {
        TEST_NAMES.forEach(function(testname) {
          it(testname, function(done) {
            expect(testname).to.not.equal("not this");
            expect(1+1).to.equal(2);
            done();
          });
        });
      });
    })
    </script>
    <script>
      mocha.run();
    </script>
    <p>Observe that there is no section called "Dynamically created tests" above, although there should be.</p>
  </body>
</html>

Most helpful comment

You actually can (ab)use before() to dynamically generate It() tests from asynchronous data. First you create a placeholder It() test that forces before() to execute, and then you generate more It() tests inside of the async callback/promise/whatever in the before() body.

Here's some sample code reproduced from my stackoverflow answer on this topic:

before(function () {
    console.log('Let the abuse begin...');
    return promiseFn().
        then(function (testSuite) {
            describe('here are some dynamic It() tests', function () {
                testSuite.specs.forEach(function (spec) {
                    it(spec.description, function () {
                        var actualResult = runMyTest(spec);
                        assert.equal(actualResult, spec.expectedResult);
                    });
                });
            });
        });
});

it('This is a required placeholder to allow before() to work', function () {
    console.log('Mocha should not require this hack IMHO');
});

All 7 comments

the it fn registers the test with the suite. you can do dynamic tests this way (move the forEach into an it)

 describe('Dynamically created tests (which do not work)', function() {        
          it('should not be named "not this"', function(done) {
            TEST_NAMES.forEach(function(testname) {
              expect(testname).to.not.equal("not this");
              expect(1+1).to.equal(2);              
          });
          done();
        });
      });

You can generate tests in this way, but before() only runs after all tests have been defined.

If you populate TEST_NAMES in the describe() directly instead of the before() your example will work as you expect.

You actually can (ab)use before() to dynamically generate It() tests from asynchronous data. First you create a placeholder It() test that forces before() to execute, and then you generate more It() tests inside of the async callback/promise/whatever in the before() body.

Here's some sample code reproduced from my stackoverflow answer on this topic:

before(function () {
    console.log('Let the abuse begin...');
    return promiseFn().
        then(function (testSuite) {
            describe('here are some dynamic It() tests', function () {
                testSuite.specs.forEach(function (spec) {
                    it(spec.description, function () {
                        var actualResult = runMyTest(spec);
                        assert.equal(actualResult, spec.expectedResult);
                    });
                });
            });
        });
});

it('This is a required placeholder to allow before() to work', function () {
    console.log('Mocha should not require this hack IMHO');
});

The answer provided by @rob3c works, though a more proper way would be to use the --delay option, which is documented here. This gives you direct control of the flow of defining setup tasks and test cases before Mocha starts running them.

Rewriting the above example with this method:

describe('here are some dynamic It() tests', function () {
    testSuite.specs.forEach(function (spec) {
        it(spec.description, function () {
            var actualResult = runMyTest(spec);
            assert.equal(actualResult, spec.expectedResult);
        });
    });
    setImmediate(run); // run() should be called after all describe functions are executed
    //run();  doing this here instead would be too soon, and mocha likes to hang.
});

--delay is a global option so run() should only be called once, after all tests are described. The above example is fine if it's the only asynchronously defined test, but if you have multiple tests across multiple files, it would be necessary to track their loading state before finally calling run().

Thanks to @boneskull for pointing out this option!

The point of using my method instead of the global run hook, is that you aren't required to globally coordinate your dynamic test generation. With my method, you can make those decisions per-file on an ad-hoc basis without requiring any global efforts to call the single run method. There's no "proper" way to do that except in the way I showed...until mocha provides proper support for it, of course.

Also, the placeholder test can be skipped since it doesn't do anything anyway:
it.skip("Placeholder needed to trigger mocha before() block", function () {});
@rob3c Do you know of a better framework that supports async dynamic test allocations?

@npopov-d3 Thanks for the skip tweak for the placeholder!

Unfortunately, I don't know of a better async testing framework offhand, as this simple hack is working for us, and we have already have a large investment in mocha. However, if you find something that has nicer async dynamic test allocation support that is backwards compatible with mocha, please let us know.

Was this page helpful?
0 / 5 - 0 ratings