This context provides a detailed guide on how to set up unit test cases for asynchronous calls using the Jest testing framework.
Abstract
The context begins by explaining the importance of unit testing in isolating and verifying individual parts of a program. It introduces Jest, a JavaScript testing framework, and discusses the challenges of testing asynchronous calls due to their non-blocking nature. The context then demonstrates the pitfalls of asynchronous calls using a test case for the setTimeout function, and explains how to detect this issue using assertions. The context also provides a fix for this issue using the 'done' callback. The latter part of the context focuses on testing promise-based asynchronous calls and provides four methods to do so. The context concludes by discussing mock functions and manual mocks, and their role in testing asynchronous calls.
Bullet points
Unit testing is crucial for isolating and verifying individual parts of a program.
Jest is a JavaScript testing framework used for ensuring the correctness of any JavaScript codebase.
Asynchronous calls are challenging to test due to their non-blocking nature.
The pitfall of asynchronous calls is demonstrated using a test case for the setTimeout function.
Assertions can be used to detect issues with asynchronous calls.
The 'done' callback can be used to fix issues with asynchronous calls.
Four methods are provided for testing promise-based asynchronous calls.
Mock functions and manual mocks are discussed in the context of testing asynchronous calls.
Test and Mock Asynchronous Calls With the Jest Testing Framework
A detailed guide on how to set up unit test cases for asynchronous calls
Unit testing isolates each part of the program and verifies that the individual parts are correct. Unit test cases are typically automated tests written and run by developers. This enables problems to be discovered early in the development cycle. Jest is a JavaScript testing framework to ensure the correctness of any JavaScript codebase.
Asynchronous calls don’t block or wait for calls to return. After the call is made, program execution continues. When the call returns, a callback function is executed. It’s hard to test asynchronous calls due to the asynchronous nature.
The Pitfall of Asynchronous Calls
The following is a unit test case for an asynchronous call, setTimeout.
Apparently, 1 isn’t 2, but the test passes.
This is the pitfall of asynchronous calls. Line 3 calls setTimeout and returns. The test finishes before line 4 is executed. No error is found before the test exits — therefore, the test case passes.
Well, it’s obvious that 1 isn’t 2. However, for a complicated test, you may not notice a false-positive case.
How can we detect the issue?
The detection: Use assertions
Here’s Jest’s definition of assertions:
"expect.assertions(number) verifies that a certain number of assertions are called during a test. This is often useful when testing asynchronous code, in order to make sure that assertions in a callback actually got called.”
We can add expect.assertions(1) at line 3.
This change ensures there will be one expect executed in this test case. Therefore, since no expect is called before exiting, the test case fails as expected.
If there are nexpect statements in a test case, expect.assertions(n) will ensure n expect statements are executed. The order of expect.assertions(n) in a test case doesn’t matter. If you move line 3 to line 6, it works too.
If you don’t care how many times the expect statement is executed, you can use expect.hasAssertions() to verify that at least one assertion is called during a test.
We’re able to detect the issue through assertion. How can we fix the problem?
The fix: Use the ‘done’ callback
We can fix this issue by waiting for setTimeout to finish.
We pass in Jest’s done callback to the test case at line 2 and wait for setTimeout to finish. And then we invoke done() to tell Jest it can exit now. With the help of the done callback, this test case fails as expected.
It’s always a good idea to have assertion to ensure the asynchronous call is actually tested.
Test Promise-Based Asynchronous Calls
We handled callback-based asynchronous calls, such as setTimeout. How about promise-based asynchronous calls?
Will the following test case pass?
The test case fails because getData exits before the promise resolves. It fails upon line 3’s assertion.
How can we fix the problem?
Simply add return before the promise. Since it returns a promise, the test will wait for the promise to be resolved or rejected. Therefore, the expect statement in the then and catch methods gets a chance to execute the callback.
There are four ways to test asynchronous calls properly.
Method 1: Add ‘return’ before the promise’s ‘then’ and catch calls
With return added before each promise, we can successfully test getDataresolved and rejected cases.
Method 2: Add ‘return’ before the ‘expect’ ‘.resolves’ and ‘.rejects’ calls
Jest provides .resolves and .rejects matchers for expect statements. These matchers will wait for the promise to resolve. Of course, you still need to add return before each expect statement.
Method 3: Use ‘async’ and ‘await’ calls without ‘return’
You can also use async and await to do the tests, without needing return in the statement.
At line 2 and line 7, the keyword async declares the function returns a promise. At line 4 and line 10, the keyword await makes JavaScript wait until the promise settles and returns its result.
Method 4: Apply the ‘expect’ ‘.resolves’ and ‘.rejects’ calls to ‘async’ and ’await’
expect’s .resolves and .rejects can be applied to async and await too. There’s also no need to have return in the statement.
Mock Functions
Sometimes, we want to skip the actual promise calls and test the code logic only. Mock functions help us to achieve the goal. There are two ways to mock functions:
Create a mock function to use in test code.
Write a manual mock to override a module dependency.
Let’s take a look at mock functions first.
jest.mock(moduleName, factory?, options?) mocks a module with specific name. factory and options are optional. We have a module, PetStore/apis, which has a few promise calls.
We can mock them in a test suite:
We have mocked all three calls with successful responses. How about reject cases?
We can change the return values from Promise.resolve to Promise.reject.
What if we want to test some successful cases and some failed cases?
In fact, Jest provides some convenient ways to mock promise calls.
These methods can be combined to return any promise calls in any order.
Line 2 mocks createPets, whose first call returns successful, and the second call returns failed.
Lines 3–20 mock listPets, whose first call returns a one-item array, and the second call returns failed, and the rest calls return a two-item array.
Line 21 mocks showPetById, which always returns failed.
Don’t these mock functions provide flexibility?
If you have mocked the module, PetStore/apis, you may want to unmock it after the tests.
Jest provides a number of APIs to clear mocks:
jest.clearAllMocks(): It clears the mock.calls and mock.instances properties of all mocks.
jest.resetAllMocks(): It resets the state of all mocks. In addition to jest.clearAllMocks(), it also removes any mocked return values or implementations.
jest.restoreAllMocks(): It restores all mocks back to their original value. In addition to jest.resetAllMocks(), it also restores the original (non-mocked) implementation.
Jest also provides a number of APIs to setup and teardown tests.
beforeAll(fn): It runs a function before any of the tests in this file run.
afterAll(fn): It runs a function after all the tests in this file have completed.
beforeEach(fn): It runs a function before each of the tests in this file runs.
afterEach(fn): It runs a function after each one of the tests in this file completes.
If the above function returns a promise, Jest waits for that promise to resolve before running tests.
Besides jest.mock(), we can spy on a function by jest.spyOn(object, methodName, accessType?). It creates a mock function similar to jest.fn() but also tracks calls to object[methodName]. It returns a Jest mock function. jest.spyOn() takes an optional third argument of accessType that can be either 'get' or 'set', if you want to spy on a getter or a setter, respectively.
Assume that we have mocked listPets to jest.fn().mockRejectedValue([]), and ACallThatInvolveslistPets() writes a console.error before the promise is rejected, the following test will pass. However, the console.error will be executed, polluting the test output.
In 6 Ways to Run Jest Test Cases Silently, we have discussed how to turn off console.error. The solution is to use jest.spyOn() to mock console.error() to do nothing.
Line 3 creates a spy, and line 5 resets it.
In addition, the spy can check whether it has been called.
At line 4, spy is called 0 time, but at line 6, spy is called 1 time.
jest.spyOn() is very effective in this case.
Manual Mocks
Sometimes, it is too much hassle to create mock functions for individual test cases. We can choose manual mocks to mock modules. Here, axios is used as an example for manual mock.
Manual mocks are defined by writing a module in a __mocks__ subdirectory immediately adjacent to the module. If the module to be mocked is a Node module, the mock should be placed in the __mocks__ directory adjacent to node_modules.
If a manual mock exists for a given module, like the examples above, Jest will use that module when explicitly calling jest.mock('moduleName'). However, node modules are automatically mocked if there’s a manual mock in place.
Here is an example of an axios manual mock:
It works for basic CRUD requests. This is the whole process on how to test asynchronous calls in Jest. A similar process can be applied to other promise-based mechanisms.
Caveats: For axios, though, this manual mock doesn’t work for interceptors. The alternative is to use jest or NODE_ENV conditionally adding interceptors.
Conclusion
We walked through the process of how to test and mock asynchronous calls with the Jest testing framework.
Thanks for reading. I hope this was helpful. You can see my other Medium publications here.