How to Test Asynchronous Code with Jest
Aug 23, 2019 • 9 Minute Read
Introduction
Asynchronicity is a fundamental concept of the web today. Due to the single-threaded nature of the JavaScript event loop, executing code in a non-blocking way is critical to building efficient and fast programs. In this guide, we will explore the different ways Jest give us to test asynchronous code properly.
False Positives on Tests
Jest typically expects to execute the tests’ functions synchronously. If we do an asynchronous operation, but we don't let Jest know that it should wait for the test to end, it will give a false positive.
For example, the following test shouldn't pass:
test("this shouldn't pass", () => {
setTimeout(() => {
// this should fail:
expect(false).toBe(true);
});
});
The above test is a false positive. The test will pass but the assertion should make it fail. Jest will run the test function and, since the test function simply starts a timer and then ends, Jest will assume the test passed. The test completes early, and the expectation will run sometime in the future when the test has already been marked as passed.
Tests passing when there are no assertions is the default behavior of Jest. If you want to avoid Jest giving a false positive, by running tests without assertions, you can either use the expect.hasAssertions() or expect.assertions(number) methods. These two methods will ensure there's at least a certain number of assertions within the test function before assuming the test passes. They are designed to be called manually in every test, which is quite inconvenient.
If you are transpiling your code with Babel and you want to change the behavior globally, you can use the babel-jest-assertions plugin. This plugin will inject a call to expect.hasAssertions and expect.hasAssertionsNumber(N)in every test by counting how many assertions exist in the source code of the test function body.
We need to inform Jest that it should wait until the assertions run. How we do this depends on which asynchronous patterns the code we want to test uses.
Asynchronous Patterns
There are several patterns for handling async operations in JavaScript; the most used ones are:
- Callbacks
- Promises
- Async/Await
Testing Callbacks
Callbacks are one of the most used patterns for handling actions that happen in the future. They are simple yet powerful building blocks of asynchronous programming in JavaScript. Callbacks work because functions are first-class citizens, and we can use functions as argument values when we invoke other functions. We can pass a callback function to the operation we want to execute. Sometime later our callback will be invoked, which usually has the result of the async operation being passed as an argument.
For callback-based code, Jest provides a done callback as the argument of the test function. We should invoke this function after we are done asserting.
For example, let's say we have an asynchronous add function which waits for a half-second to produce a result:
function addAsync(a, b, callback) {
setTimeout(() => {
const result = a + b;
callback(result);
}, 500)
}
We can test the above function like this:
test('add numbers async', done => {
addAsync(10, 5, result => {
expect(result).toBe(15);
done();
})
})
Since the test function defines the done parameter, Jest will wait for expectations until done() is invoked. Jest, by default, will wait for up to five seconds for a test to complete. Otherwise, the test will fail.
Testing Promises
ES6 introduced the concept of Promises. They are a lightweight abstraction layer that provides a wrapper to represent the future value of an operation that is usually asynchronous. Promises fix a lot of problems from callback-based APIs. They allow us to overcome issues like the infamous callback-hell and the drawbacks of inversion of control.
Inversion of control, in this context, means that we give out the flow, or control, of our code - what we do in the callback - to the asynchronous operation. This operation can be out of our control. Instead of relying on external code to have our callback invoked with promises, we get a time-independent representation of the value. We can treat it just like any other value.
When testing Promise-based APIs, again, we will have false positives if we don't let Jest know that we are working with asynchronous code.
For example, the following test should also fail:
test('should not pass', () => {
const p = Promise.resolve(false);
p.then(value => {
expect(value).toBe(true);
})
})
The above test will incorrectly pass because Jest is not aware that we are doing an asynchronous operation. Depending on the version of Node.js on your system, you will only get an UnhandledPromiseRejectionWarning on the console, but the test will be marked as passed.
The simplest way to let Jest know that we are dealing with asynchronous code is to return the Promise object from the test function. You can, for example, evaluate the expectations in the then callback:
//...
test('properly test a Promise', () => {
return somePromise.then(value => {
expect(value).toBeTrue();
})
})
test('should resolve to some value', () => {
const p = Promise.resolve('some value');
return expect(p).resolves.toBe('some value');
});
test('should reject to error', () => {
const p = Promise.reject('error');
return expect(p).rejects.toBe('error');
});
Note that these matchers also return a Promise object; that's why we must return the assertion. If we don't return it, we will have false positives again.
Testing With Async / Await
As we saw in the previous section, Jest will know that we are dealing with asynchronous code if we return a Promise object form the test function. If we declare the test function as async, it will implicitly make the function to return a Promise. We can also use the await keyword to resolve Promise values and then assert them as if they were synchronous.
For example, we can wait for the resolved Promise value and assert it, like so:
test('shows how async / await works', async () => {
const value = await Promise.resolve(true);
expect(value).toBe(true);
});
This approach is very convenient. It lets you run the expectations just as if the values were synchronous. In the end, since we are waiting for the asynchronous values, the Promise that the test function returns will make Jest aware of the need to wait. Also, if you miss the await keyword, the test will fail because it is expecting some value, not a Promise.
Conclusion
In this guide, we have explored the main asynchronous patterns on JavaScript and the ways to test them with Jest. We explained why it is so common to have false positives, as well as how to detect and avoid them.
Keep in mind that unit tests should be fast. Keeping your unit tests fast often means that you shouldn't be accessing external resources, like remote servers, databases, or even the file system. Usually, Integration Tests cover the interactions with this kind of high-level resources.
When writing unit tests where the code interacts with asynchronous entities, I always recommend building contracts through APIs that expose an abstraction to the resource we want to use. We can then replace these dependencies with mocks in our tests.
Model your code to be testable. If you find yourself struggling to test a piece of code, it might be a good idea to step back and review the design, interactions, and the abstractions on which it is dependent. Poorly designed code is almost always hard to test.
If your tests rely on timers, you can simulate the passage of time in Jest by using timer mocks and keep your unit tests blazing fast.
See my related guide on Jest Mock Functions: Mock Functions or Spies Demystified - How Does jest.fn() Work?.