Testing asynchronous code

The rest of the methods provided by our service are asynchronous and use the Fetch API. To test those methods, we need to discover how to test asynchronous code with Jest and how to use mocks (for example, to avoid side effects when the Fetch API is called).

First of all, the good news is that Jest fully supports testing asynchronous code in general, whether that code works with callbacks, Promises, or async/await. You can find the related official documentation here: https://jestjs.io/docs/en/asynchronous. Since we're using async and await, we'll focus on them.

We can simply create an asynchronous test case by adding the async keyword in front of the test function definition:

it('should fail if the response does not have a 2xx status code', async () ⇒ { 
    await doSomethingAsync(); 
    ...​ 
}); 

What we also need now is a polyfill for the Fetch API, since our tests will run in a Node environment, where Fetch is not available. We'll use cross-fetch (https://www.npmjs.com/package/cross-fetch) for this. Install cross-fetch using npm install --save-dev cross-fetch.

Notice that, this time, we've used --save-dev since this dependency is only needed for tests.

In addition, we'll need a mock for the Fetch API. For this specific mocking need, we'll use the jest-fetch-mock utility library (https://www.npmjs.com/package/jest-fetch-mock):

  1. Install it using npm install jest-fetch-mock --save-dev.
  2.  Create a file called jest.setup.ts at the root of the project (that is, next to jest.config.js) and add this to it:
import {GlobalWithFetchMock} from "jest-fetch-mock"; 

const customGlobal: GlobalWithFetchMock = global as GlobalWithFetchMock; 

// load the mock for the Fetch API: https://www.npmjs.com/package/jest-fetch-mock 
// and set it on the global scope 
customGlobal.fetch = require('jest-fetch-mock'); 
customGlobal.fetchMock = customGlobal.fetch; 

In this file, we override the global fetch variable and we also assign a global fetchMock variable. This will make our tests clutter-free.

  1. Now, edit the jest.config.js file to make sure that our setup script is loaded when Jest starts. Add the following property to it:
setupFiles: [ 
    './jest.setup.ts' 
]

By doing this, we've instructed Jest to execute the jest.setup.ts file when it initializes, right before running our tests. In that file, we've loaded the Fetch API mock and declared it in the global scope, which is not problematic in this specific case. Thanks to this, we'll now be able to control what the Fetch API calls return, using the API documented here: https://www.npmjs.com/package/jest-fetch-mock#api.

Let's start off with a simple test and test our checkResponseStatus function as follows:

  1. Create a dedicated group of tests for it within population-service.test.ts:
describe('checkResponseStatus', () => { 
    ... 
}); 
  1. Now add a first test inside the describe block:
it('should fail if no response object is passed', async () => { 
    await expect(sut.checkResponseStatus(null as unknown as 
Response)).rejects.toThrow(); });

Here, we make use of the async testing support of Jest to call checkResponseStatus. Notice that our test expects the promise to reject/throw using rejects.toThrow().

  1. Now, add additional tests for the edge cases of the method:
it('should fail if the status is below 200', async () => { 
    await expect(sut.checkResponseStatus(new Response(null, { 
        status: 199 
    }))).rejects.toThrow(); 
}); 
 
it('should fail if the status is above 299', async () => { 
    await expect(sut.checkResponseStatus(new Response(null, { 
        status: 300 
    }))).rejects.toThrow(); 
}); 
You can execute (and debug!) Jest tests using most IDEs. For example, IntelliJ Ultimate has built-in support for Jest: https://www.jetbrains.com/help/idea/running-unit-tests-on-jest.html. For Visual Studio Code, you can install this extension: https://marketplace.visualstudio.com/items?itemName=Orta.vscode-jest.
  1. Finally, let's not forget to test the positive case:
it('should succeed if the response has a 2xx status code', async () => { 
    let fakeResponse: Response = new Response(null, { 
        status: 200 
    }); 

    await expect(sut.checkResponseStatus(fakeResponse))
.resolves.toBe(fakeResponse); fakeResponse = new Response(null, { status: 204 }); await expect(sut.checkResponseStatus( fakeResponse)).resolves.toBe(fakeResponse); fakeResponse = new Response(null, { status: 299 }); await expect(sut.checkResponseStatus( fakeResponse)).resolves.toBe(fakeResponse); });

This time, since we provide valid input to the function, we, of course, verify that the promise resolves to the expected value.

An alternative style to this is to add a done argument to the test function and to invoke it when the test is done or to invoke done.fail when an error is detected within the test.

Here's a nice article about asynchronous testing with Jest: https://medium.com/@liran.tal/demystifying-jest-async-testing-patterns-b730d4cca4ec.

Let's apply this knowledge to test the getAllCountries method.