Search code examples
javascriptmocha.js

How to test async code with mocha using await


How do I test async code with mocha? I wanna use multiple await inside mocha

var assert = require('assert');

async function callAsync1() {
  // async stuff
}

async function callAsync2() {
  return true;
}

describe('test', function () {
  it('should resolve', async (done) => {
      await callAsync1();
      let res = await callAsync2();
      assert.equal(res, true);
      done();
      });
});

This produces error below:

  1) test
       should resolve:
     Error: Resolution method is overspecified. Specify a callback *or* return a Promise; not both.
      at Context.it (test.js:8:4)

If I remove done() I get:

  1) test
       should resolve:
     Error: Timeout of 2000ms exceeded. For async tests and hooks, ensure "done()" is called; if returning a Promise, ensure it resolves. (/tmp/test/test.js)

Solution

  • async/await is nothing more than syntactic sugar on top of Promises; which Mocha natively supports.

    Therefore, all you need is:

    async function getFoo() {
      return 'foo'
    }
    
    // fixes applied:
    // 1. no 'done' parameter
    // 2. replaced arrows/lambdas with plain functions.
    describe('#getFoo', function () {
      it('returns foo', async function () {
        const result = await getFoo()
        assert.equal(result, 'foo')
      })
    })
    

    ... there you go, async/await in Mocha.

    if you're skimming this just grab the above MCVE, note the following key points and you're good to go:

    • Do not declare done as an argument.

      The Resolution method overspecified exception is thrown because you both: return a Promise and also declare done. Promises and async/await don't use done, at all.

    • Promises and async/await are equivalent.

    • Arrow functions (lambdas) in Mocha's callback should be avoided.

      They lexically bind this so shouldn't be used when declaring the callbacks of describe,it,before and so on. The test code itself that's tested within the same callbacks can contain whatever though. If you ever read this in a book, throw it away. This is bullshit of the highest degree. Functions and lambdas aren't perfectly interchangeable nor where they ever designed to be.

    Actual answer

    Right, so an actual answer is gonna be a bit long because it involves a bunch of discreet subjects, but generally speaking:

    • async/await and Promises are equivalent. In fact, async functions always implicitly return a Promise.

    • Mocha (wrongly IMO) assumes you're aware of this & prominently shows examples of Promise-based code instead of explicit async/await.

    • Mocha has 4 different types of tests; it attempts to resolve the type automatically but it requires that you setup the test in a specific way. Declaring done tells Mocha this is a callback test, while returning a Promise tells it it's a Promise-based test. You do both so it freaks out, basically asking you to choose one or the other.

    • Theres a specific way you can get both done and Promises working as a Mocha test. As explained this is wrong yet it doesn't produce an error. However, that structure entirely misses the point of Promises and it's almost certain you'll write your entire test suite in an incorrect and brittle manner if you keep going. Make sure you avoid this.

    Now to get to the point quickly, here's what you do wrong, plain and simple:

    (click on the sketch to zoom-in)

    shows a Mocha it() function. It's callback is declared as async. Drawn arrows point to a "done" argument of the it function callback, recommending to remove it. Some additional text recommends to avoid arrow functions in Mocha's describe, it and any other hook function and their callbacks. Finally points out that returning anything from the it callback is unecessary unless explicitly testing Promise code

    Very specifically, avoid this setup at all costs: It's the worst because it's incorrect but it might look like it works .. for a while.

    shows the worst example. A non-async it callback that declares done parameter. A Promise test calls the done parameter in  its callback. Mocha doesnt complaint about it but its an incorrect way of using Promises

    This is a correct example:

    shows the same it as above image but with all recommendations fixed.

    For accessibility reasons, resummarised:

    • declare your it()-callbacks as an async function.
    • Remove any done parameter from it() callbacks.
      never mix done (in fact don't ever declare any parameters) when testing Promise-based or async/await syntax.

    expanding on each:

    async/await and Promises are equivalent.

    async/await is a mechanism built on top of, and semantically equivalent to Promises, for lack of a better word.

    Anyone telling you otherwise is wrong. Theres no degrees of freedom here. This "detail" is the opening line of the TC39 proposal itself:

    The introduction of promises and generators in ECMAScript presents an opportunity to dramatically improve the language-level model for writing asynchronous code in ....

    Mocha's documentation assumes you know this and just gives out a Promise example that; if you only recently got involved with the language, you wouldn't know any better because async/await is ubiquitous now.

    Long-story short, whenever you see "supports Promises" you're safe to assume 100% that it also means, "works with async/await".

    So back to Mocha.

    Promise-based tests are explained in the docs. You just have to return the Promise to it()'s callback.

    Then:

    • If the Promise resolves then the test passes.
    • if the Promise rejects then the test fails.

    It's really as simple as that.

    Now, bearing in mind that async functions always implicitly return a Promise you could simply do the following:

    // technically returns a Promise which resolves with 'foo'
    async function getFoo() {
      return 'foo'
    }
    
    describe('#getFoo', function () {
      it('resolves with foo', function () {
        return getFoo().then(result => {
          assert.equal(result, 'foo')
        })
      })
    })
    

    ... and therefore:

    
    // technically returns a Promise which resolves with 'foo'
    async function getFoo() {
      return 'foo'
    }
    
    describe('#getFoo', function () {
      it('resolves with foo', async function () {
        assert.equal(result, 'foo')
      })
    })
    

    No difference between async/await and Promise.

    Resolution method is overspecified

    This exception is thrown by Mocha when you setup your test in such a way that its not entirely clear what your intentions are.

    done is declared and (manually called) in an archaic model of asynchronous programming that uses callback fuctions.
    It's rare that you see code like this anymore unless youre working with legacy systems.

    So for all intents and purproses, unless you know what you're doing, DO NOT declare done as a function argument.

    If you use any of the methods I described above - which involve Promise and/or async/await- you need to remove done completely from your code.

    That includes (especially this) passing it as an argument to its callback because that trips up the internal mechanism that hints to Mocha that your intent isn't to actually run a Promise-based test but rather a done/callback-based test.

    I explain this clearly in the sketch above but here it is again:

    async function getFoo() {
      return 'foo'
    }
    
    describe('#getFoo', function () {
      it('resolves with foo', function (done) { // <-- !!
        return getFoo().then(result => {
          assert.equal(result, 'foo')
        })
      })
    })
    

    This throws that exception because you're already returning a Promise so it's classified as a Promise test which doesn't need done since it has everything necessary to know when the asynchronous action has ended.

    Mocha test types

    The important context here is that Mocha supports 4 different types of tests.

    It's crucial that you pick one style and roll with it all the way because Mocha makes assumptions about the type of test based on how you set it up.

    The mere existence of an argument in it, (in this case done) is hinting to Mocha that the test is callback-based and not Promise/async-based. It's just how the internals in Mocha attempt to figure out what your intent is.

    • Purely Promise-based: return the Promise to the it callback and use then/catch handlers to perform assertions.
    • Promise-based and async/await: Mark the it callback as async and use await to perform assertions.
    • Callback-based via done: do asynchronous operations and assertions and when you're done, explicitly call done(). This is a relic from the old days when we had to deal with callbacks, before Promise or async/await came about. It still has some use cases, mainly testing callback-style code or events.
    • Purely synchronous: You don't need to return anything nor declare any special arguments.

    Again you return both a Promises and also declare done as an argument, Mocha will start complaining:

    Error: Resolution method is overspecified. Specify a callback or return a Promise; not both

    Because it's conflicted as to which type of test you want to use.

    The done method is only used for testing callback-based or event-based code. You shouldn't use it if you're testing Promise-based or async/await functions. .

    asynchronous programming vs async keyword

    Just to settle any confusion about terminology:

    Asynchronous is a Greek word meaning not in synchrony = not following in some apparent manner = not one-code-line-after-the-other*. This is a general programming model thats central to JavaScript since its inception. Shortened it's also called "async" code.

    Confusingly enough, async also exists as a language keyword that marks a function as asynchronous in nature.

    The point is that asynchronous operations and the async keyword revolve around the same concept but they are not the same thing.

    You can handle asynchronous operations with other constructs apart from async functions, e.g: Promises, callback functions, events etc. If I'm highlighting async I'm talking about the keyword otherwise I'm talking about the general concept.