DEV Community

Cover image for Testing with Jest & async/await
Paula Santamaría
Paula Santamaría

Posted on

Testing with Jest & async/await

If you read my previous post (Testing Node.js + Mongoose with an in-memory database), you know that the last couple of weeks I've been working on testing a node.js and mongoose app.

I'm a big fan of async/await in javascript. After all, I've seen callback hell and it's not pretty. So naturally, when I started writing my tests there was a lot of async code that needed testing and I came across some issues that I had to figure out for my tests to work properly.

In this post I'll share some real examples that'll help you test your async javascript code using Jest.

Table of contents

Testing async functions

Here's how a test suite for async code should look like:

describe('scope ', () => {
    it('works with async', async () => {
        /* Some async code testing. */
    });
});
Enter fullscreen mode Exit fullscreen mode

Notice that the function inside describe is not async, but the one in it is.

Seed some data to test

Sometimes we need to seed our test database to have some data to work with. I'll show you two ways to achieve this:

a. Add the data you require inside each test

Check the following code:

it('should retrieve the correct product if id matches', async () => {
    // Seed.
    const createdIphone = await productModel.create(productIphone);

    // test
    const foundProduct = await productService.getById(createdIphone.id);

    expect(foundProduct.id).toBe(createdIphone.id);
    expect(foundProduct.name).toBe(productIphone.name);
});
Enter fullscreen mode Exit fullscreen mode

The data is seeded at the beginning of the test and used later. This method is useful when we only need this particular data for this particular test. If you find yourself copy-pasting that first line in other test, consider the following method.

b. Seed the data using beforeEach

Instead of adding the data in every test, simply add it inside the beforeEach() method like so:

beforeEach(async () => await createProducts());
afterEach(async () => await dbHandler.clearDatabase());

describe('product ', () => {
    it('test that needs data', async () => {

    });

    it('another test that needs data', async () => {

    });
});
Enter fullscreen mode Exit fullscreen mode

This way the products will be added before each test, and removed after each test, ensuring that each test has a clean start.

await first and expect later

Since we're using async we can await the results of our functions and then use expect to verify the results, like so:

it('should retrieve the correct product if id matches', async () => {
    const foundProduct = await productService.getById(productIphoneId);

    expect(foundProduct.id).toBe(productIphoneId);
    expect(foundProduct.name).toBe(productIphone.name);
});
Enter fullscreen mode Exit fullscreen mode

Use resolves to await the result

Another way of testing the results of an async function is with resolves which will result in Jest waiting for the async function to finish executing.

In the following example, we wait for getById to resolve and then we check if the result is null:

it('should return null if nothing is found', async () => {
    // mongoose.Types.ObjectId() generates a new ID that won't exist in the current database.
    await expect(productService.getById(mongoose.Types.ObjectId()))
        .resolves
        .toBeNull();
});
Enter fullscreen mode Exit fullscreen mode

Test error handling

Test that a function doesn't throw an error

We can expect for an async function not to throw an error, like so:

it('can be created correctly', async () => {
    expect(async () => await productService.create(productComplete))
        .not
        .toThrow();
});
Enter fullscreen mode Exit fullscreen mode

Test that a function throws the correct error

We can use rejects to wait for an async function to resolve with error, and then combine it with toThrow to make sure the error thrown is the one we expect.

it('requires name and price', async () => {

    await expect(productService.create(productMissingName))
        .rejects
        .toThrow(mongoose.Error.ValidationError);

    await expect(productService.create(productMissingPrice))
        .rejects
        .toThrow(mongoose.Error.ValidationError);
});
Enter fullscreen mode Exit fullscreen mode

Try it out yourself

Here's a Github repo where I put together all of the examples included in this article:

GitHub logo pawap90 / test-mongoose-inmemory

A sample project that demonstrates how to test mongoose operations through jest with an in-memory database.

A Node.js + Mongoose + Jest sample project that demonstrates how to test mongoose operations using Jest with an in-memory database.

This repo was build as an example for my article Testing Node.js + Mongoose with an in-memory database.

Dependencies

What you need to run this project:

  • Node.js

(MongoDB is not required because it'll run in memory, handled by the package mongodb-memory-server).

Try it out

1. Install dependencies

npm install

2. Run tests

npm test

Contribute

Feel free to contribute to this project either by leaving your comments and suggestions in the Issues section or creating a PR. More and diverse test examples are always useful. Make sure to take a look at Jest docs and the existent examples to avoid repeating.

Tools

Main tools used in this project:

Also take a look at mongodb-memory-server-global to download mongod's binary globally and…





I created this repo for my previous post, but I've improved it and included more examples for this article.

More resources

Top comments (4)

Collapse
 
zyabxwcd profile image
Akash • Edited

Disclaimer: I am really new to testing (like its my day 3 of learning unit testing with Jest), so please bear with me if I am misinformed or confused, I am feeling pretty dizzy by all the reading on Jest unit testing. Thanks in advance :)

The product model and service you have used in the seeding data section, they are mocks right? They are not actually interacting with the DB? If there are, isn't that against a unit test and makes it an integration test? Secondly, if they are mocks, how exactly are you managing to get the same record using getById as the one you are creating with the model function? Is there any way that for a single test case, the record that is created by the mocked "productModel.create" will be the one returned by "productService.getById"? I would like to avoid hardcoding initialisation data, like I mean I would not want the record to be same for each of the test case and therefore mock implementation for productModel.create and productService.getById like below is not to my liking.
const data = { name: 'Jon Doe' };
productModel.create = jest.fn().mockResolvedValue(data);
productService.getById = jest.fn().mockResolvedValue(data);

Would we have to like create a mock class with the instance variable storing the record and the class will have one getter and one setter to operate on the same data. We can then assign the mocks of productModel.create to the setter and productService.getById to the getter. This way, maybe we can somehow manage different mock data, created at will to our liking without conflicts cause instance variable will differ per class instance we create. Is this even a good approach to follow or is this an anti-pattern/bad-way and interacting with the actual DB as a trade-off for the complexity of the above approach much more likeable?

I hope I am making sense.

Collapse
 
slidenerd profile image
slidenerd

should you add the done parameter or not, some places add it , some dont

Collapse
 
paulasantamaria profile image
Paula Santamaría

As far as I know, the done parameter is only needed when testing code with callbacks. I never use it with async/await

Collapse
 
jamesbond_ profile image
JamesBond

Very useful, thanks!