Unit testing async functions
Tags: testing, javascript
Published: 2021-03-30 20:07:39

If you're just getting comfortable writing `async` functions in JavaScript (or using Promises in general), a potential stumbling block you might run into is writing proper unit tests for them.

The good news is that as long as your test framework provides a way to write expectations for _resolved values_ and _rejected values_ (usually Errors), adjusting your unit tests should be relatively simple. To give some examples, I'll show some simple positive and negative unit tests for async functions using three popular test frameworks - *Jasmine*, *Jest*, and *Mocha* + *Chai*.

### Code under test

Before we start testing, we need an example of an asynchronous function to test, right? Let's check if a string is a palindrome:

```js
async function palindrome(value) {
    if (typeof value !== 'string') {
        throw new Error(`${value} is not a string`);
    }
    let chars = value.toLowerCase().replace(/[^a-z]+/g, '');
    return [...chars].reverse().join('') === chars;
}
```

(This function doesn't have to be asynchronous, but let's consider it a stand-in -- perhaps our real palindrome checker is on a server and the `palindrome()` function actually makes a REST call, etc.)

### Jasmine

[Jasmine](https://github.com/jasmine/jasmine) has been around a long time and remains one of my favorite test frameworks -- it's tiny, fast, and has no dependencies. It comes out of the box with async matchers, although you need to remember that _asynchronous expectations_ in Jasmine must be made using the special `expectAsync` function instead of the usual `expect` function.

```js
describe('palindrome', () => {
    it('returns true if the string is a palindrome', async () => {
        // You can await for value, then do a normal expect
        expect(await palindrome(`Madam, I'm Adam`)).toBe(true);

        // Or, you can do an asynchronous expectation
        await expectAsync(palindrome(`Madam, I'm Adam`)).toBeResolvedTo(true);
    });

    it('raises an error if the value is not a string', async () => {
        await expectAsync(palindrome(37)).toBeRejectedWithError(/.+ is not a string/);
    });
});
```

For positive expectations, I prefer _awaiting for a value first_ and then using a standard expect -- this is more flexible, because you can use any Jasmine matcher (like `toBeInstanceOf`, `toContain`, etc.). If you use the asynchronous expect, you can only make an equality comparison.

For negative expectations, you don't have the option of waiting for a value (the rejected promise would fail the test). In this example I've used a regular expression, but we can also pass a string or an Error object (the API for `.toBeRejectedWithError` is consistent with Jamine's `.toThrowError`).

### Jest

[Jest](https://github.com/facebook/jest) is the opposite of Jasmine, with its huge install footprint and slower runtime, but is immensely popular nowadays (especially for React testing). Like Jasmine, Jest comes with async matchers out of the box.

```js
describe('palindrome', () => {
    it('returns true if the string is a palindrome', async () => {
        // You can await for value, then do a normal expect
        expect(await palindrome(`Step on no pets`)).toBe(true);

        // Or, you can do an asynchronous expectation
        await expect(palindrome(`Step on no pets`)).resolves.toBe(true);
    });

    it('raises an error if the value is not a string', async () => {
        await expect(palindrome(37)).rejects.toThrow(/.+ is not a string/);
    });
});
```

Notice how in Jest, you can `await expect` for asynchronous expectations (there's not a separate function), and instead of using separate matchers, you can use the chaining functions `.resolves` or `.rejects` to "unwrap" a Promise and then use a normal expectation matcher. I think this is one of the better matching APIs out there.

### Mocha + Chai

[Mocha](https://github.com/mochajs/mocha) is a popular test framework that _doesn't_ bundle its own assert/expect library, which makes it very flexible but also requires installing a few more packages to setup your test environment.

For this example, I am using Mocha, plus [Chai](https://github.com/chaijs/chai) for its BDD `expect` syntax and the [chai-as-promised](https://github.com/domenic/chai-as-promised) plugin for asynchronous matchers.

```js
describe('palindrome', () => {
    it('returns true if the string is a palindrome', async () => {
        // You can await for value, then do a normal expect
        expect(await palindrome(`Never odd or even`)).to.equal(true);

        // Or, you can do an asynchronous expectation
        await expect(palindrome(`Never odd or even`)).to.eventually.equal(true);
    });

    it('raises an error if the value is not a string', async () => {
        await expect(palindrome(37)).to.be.rejectedWith(/.+ is not a string/);
    });
});
```

For positive expectations, the chai-as-promised library gives you the `.eventually` chain, which works just like Jest's `.resolves` chain and allows you to append any other regular matcher. For negative expectations, it works more like Jasmine -- there's a special `rejectedWith` matcher. Just like the other two frameworks, you can pass an Error object, a string, or a regular expression.

### Summary

Of the three test frameworks above, I think Jest has the best, most consistent style for writing asynchronous expectations. Personally, I'll drop back to Mocha or Jasmine for small tools and libraries, because I like the smaller footprint, but all 3 frameworks are quite close -- the same _functionality_ and _testing patterns_ are available in all, and your choice boils down to which particular flavor of syntax sugar you prefer.

Is there a test runner or framework you prefer (maybe one not mentioned above)? Let me know!

_Photo by <a href="https://unsplash.com/@franki?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Franki Chamaki</a> on <a href="https://unsplash.com/s/photos/unit-tests?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a>_