2

I'm trying to create mocha tests for my controllers using a config that has to be loaded async. Below is my code. However, when the mocha test is run, it doesn't run any tests, displaying 0 passing. The console.logs are never even called. I tried doing before(next => config.build().then(next)) inside of the describe, but even though the tests run, before is never called. Is there a way to have the config be loaded one time before any tests are run?

'use strict';

const common = require('./common');
const config = require('../config');

config
    .build()
    .then(test);


function test() {
console.log(1);
    describe('Unit Testing', () => {
console.log(2);
        require('./auth');
    });
}
Louis
  • 146,715
  • 28
  • 274
  • 320
terpak
  • 1,131
  • 3
  • 17
  • 35
  • This is how I am using `before`, which isn't being called: `describe('Unit Testing', () => { before(next => config.build().then(next)); require('./auth'); });` – terpak Jul 08 '16 at 13:57

3 Answers3

4

You should run Mocha with the --delay option, and then use run() once you are done building your test suite. Here is an example derived from the code you show in the question:

'use strict';

function test() {
    console.log(1);
    describe('Unit Testing', () => {
        console.log(2);
        it("test", () => {
            console.log(3);
        });
    });

    // You must use --delay for `run()` to be available to you.
    run();
}

setTimeout(test, 1000);

I'm using setTimeout to simulate an asynchronous operation. Using --delay and run() allows you to build a suite that is the result of an asynchronous computation. Note, however, that the suite must be built in one shot. (You cannot have an asynchronous process inside describe that will make calls to it. This won't work.)


One thing you should definitely not do is what rob3c suggests: calling describe or it (or both) from inside a hook. This is a mistake that every now and then people make so it is worth addressing in details. The problem is that it is just not supported by Mocha, and therefore there are no established semantics associated with calling describe or it from inside a hook. Oh, it is possible to write simple examples that work as one might expect but:

  1. When the suite becomes more complex, the suite's behavior no longer corresponds to anything sensible.

  2. Since there are no semantics associated with this approach, newer Mocha releases may handle the erroneous usage differently and break your suite.

Consider this simple example:

const assert = require("assert");

const p = Promise.resolve(["foo", "bar", "baz"]);

describe("top", () => {
    let flag;
    before(() => {
        flag = true;
        return p.then((names) => {
            describe("embedded", () => {
                for (const name of names) {
                    it(name, () => {
                        assert(flag);
                    });
                }
            });
        });
    });

    after(() => {
        flag = false;
    });

    it("regular test", () => {
        assert(flag);
    });
});

When we run it, we get:

  top
    ✓ regular test

  embedded
    1) foo
    2) bar
    3) baz

  1 passing (32ms)
  3 failing

  // [stack traces omitted for brevity]

What's going on here? Shouldn't all the tests pass? We set flag to true in the before hook for the top describe. All tests we create in it should see flag as true, no? The clue is in the output above: when we create tests inside a hook, Mocha will put the tests somewhere but it may not be in a location that reflects the structure of the describe blocks in the code. What happens in this case is that Mocha just appends the tests created in the hook the the very end of the suite, outside the top describe, so the after hook runs before the dynamically created tests, and we get a counter-intuitive result.

Using --delay and run(), we can write a suite that behaves in a way concordant with intuition:

const assert = require("assert");

const p = Promise.resolve(["foo", "bar", "baz"]).then((names) => {
    describe("top", () => {
        let flag;
        before(() => {
            flag = true;
        });

        after(() => {
            flag = false;
        });

        describe("embedded", () => {
            for (const name of names) {
                it(name, () => {
                    assert(flag);
                });
            }
        });

        it("regular test", () => {
            assert(flag);
        });
    });
    run();
});

Output:

  top
    ✓ regular test
    embedded
      ✓ foo
      ✓ bar
      ✓ baz


  4 passing (19ms)
Louis
  • 146,715
  • 28
  • 274
  • 320
  • It *is* possible to create dynamic tests on a per-file basis asynchronously. See my answer below for a way that doesn't use the `--delay` root suite delay flag or the single global `run()` hook: https://stackoverflow.com/a/48572080/871990 – rob3c Feb 01 '18 at 21:36
2

In modern environments, you can use top-level await to fetch your data up front. This is a documented approach for mocha: https://mochajs.org/#dynamically-generating-tests

Slightly adapting the example from the mocha docs to show the general idea:

function fetchData() {
  return new Promise((resolve) => setTimeout(resolve, 5000, [1, 2, 3]));
}

// top-level await: Node >= v14.8.0 with ESM test file
const data = await fetchData();

describe("dynamic tests", function () {
  data.forEach((value) => {
    it(`can use async data: ${value}`, function () {
      // do something with data here
    });
  });
});

This is nice as it is on a per-file basis, and doesn't involve you taking on management responsibility of the test runner as you do with --delay.

WickyNilliams
  • 5,218
  • 2
  • 31
  • 43
0

The problem with using the --delay command line flag and run() callback that @Louis mentioned in his accepted answer, is that run() is a single global hook that delays the root test suite. Therefore, you have to build them all at once (as he mentioned), which can make organizing tests a hassle (to say the least).

However, I prefer to avoid magic flags whenever possible, and I certainly don't want to have to manage my entire test suite in a single global run() callback. Fortunately, there's a way to dynamically create the tests on a per-file basis, and it doesn't require any special flags, either :-)

To dynamically create It() tests in any test source file using data obtained asynchronously, you can (ab)use the before() hook with a placeholder It() test to ensure mocha waits until before() is run. Here's the example from my answer to a related question, for convenience:

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');
});
rob3c
  • 1,936
  • 1
  • 21
  • 20
  • What you show is not supported by Mocha. There's no defined semantics to calling `describe` or `it` from within a `before` hook. For simple examples (like yours here) it may *look* like there are defined semantics for it, but there just aren't. When it does what you expect it to do, that's just *luck*, not *design*. I guarantee you that for more complex suites you'll run into situations where the tests you try to add in your hook will end up in locations in the test tree that appear bizarre to you. – Louis Feb 01 '18 at 21:49
  • 1
    @Louis I've used it for a couple years now without problems or surprises, so I guess I'm just lucky :-) – rob3c Feb 01 '18 at 21:59
  • Yes, you were lucky. Since what you are doing is not supported, a new version of Mocha could have come out that breaks your code. If you had opened an issue report, the answer would have been "You're trying to do something Mocha does not support" and the issue would have been closed. – Louis Feb 01 '18 at 22:10
  • 1
    Meanwhile, we're getting work done in lots of tests while it continues to work well for us in the expected way. If mocha surprises us with a breaking change, then there's nothing forcing us to upgrade. The emotion and behavior you're exhibiting suggests you're taking my suggested alternative a bit personally. Sorry to offend you. It's pretty clear I labeled it as a hack the abuses the before hook, so it's not like I tried to suggest it's a supported option blessed by the mocha project anyway. It's simple - if you don't like it, don't use it. Others do like it. – rob3c Feb 01 '18 at 22:18