Armed with how to write simple unit tests, we will now add a layer of complexity by exploring how to test asynchronous code in Jest.
Let’s return to the findRecipes()
function from the recipes
module. Remember, the findRecipes()
method will make an asynchronous REST API call and pass the resolved data to a callback function. We might use this function to find the ingredients for pesto like so:
findRecipe('pesto', (recipe) => { console.log(recipe); /* Prints { 'Basil': '2 cups', 'Pine Nuts': '2 tablespoons', 'Garlic': '2 cloves', 'Olive Oil': '0.5 cups', 'Grated Parmesan': '0.5 cups' }; */ });
If we wanted to make sure that this function does in fact get the requested data, we may be tempted to put our expect()
assertion inside the callback function like so:
test("get the full recipe for pesto", () => { //arrange const dish = "pesto"; const expectedRecipe = { 'Basil': '2 cups', 'Pine Nuts': '2 tablespoons', 'Garlic': '2 cloves', 'Olive Oil': '0.5 cups', 'Grated Parmesan': '0.5 cups' }; //act findRecipe(dish, (actualRecipe) => { //assertion expect(actualRecipe).toEqual(expectedRecipe); }); });
This logic seems fairly sound. When the API call resolves, the provided callback will be executed with the fetched data (actualRecipe
) which can be compared to expectedRecipe
.
However, this test would leave us vulnerable to a false positive, meaning it would pass even if our API call and/or assertion failed! By default, Jest is not aware that it must wait for asynchronous callbacks to resolve before finishing a test. From Jest’s perspective, it executed the findRecipe()
call and then moved on. When it didn’t immediately encounter any failing expect()
assertions, the test passed!
We can see this more clearly by replacing the assertion used above with an obviously failing assertion like expect(undefined).toBeDefined()
:
test("get the full recipe for pesto", () => { //arrange const dish = "pesto"; const expectedRecipe = { 'Basil': '2 cups', 'Pine Nuts': '2 tablespoons', 'Garlic': '2 cloves', 'Olive Oil': '0.5 cups', 'Grated Parmesan': '0.5 cups' }; //act findRecipe(dish, (actualRecipe) => { //assertion expect(undefined).toBeDefined(); // No way this passes, right? }); });
Running this test will produce a bewildering pass! Again, Jest has no way to know that the callback is asynchronous so it will not wait for it and it will not see the failing expect()
assertion.
To fix this issue, Jest allows us to add a done
parameter in the test()
callback function. The value of done
is a function and, when included as a parameter, Jest knows that the test should not finish until this done()
function is called.
Let’s take a final look at the test for the findRecipe()
function, this time using the done
parameter.
// This time we'll use the `done` parameter test("get the full recipe for pesto", (done) => { //arrange const dish = "pesto"; const expectedRecipe = { 'Basil': '2 cups', 'Pine Nuts': '2 tablespoons', 'Garlic': '2 cloves', 'Olive Oil': '0.5 cups', 'Grated Parmesan': '0.5 cups' }; //act findRecipe(dish, (actualRecipe) => { //assertion try { expect(actualRecipe).toEqual(expectedRecipe); done(); } catch (error) { done(error); } }); });
Let’s break down this example:
- In the first line of code, the
done
parameter is added to the callback passed totest()
. Jest now knows to wait until that function is called before concluding the test. - The
done()
function is called after theexpect()
assertion is made. This way, theexpect()
is guaranteed to be seen and any false-positives will be caught.
You should notice that the expect()
and done()
call are being made in a try
block. Without this, if the assertion were to fail, expect()
would throw an error before the done()
function gets a chance to be called. From Jest’s perspective, the reason for the test failure would be a timeout error (since done()
was never called) rather than the actual error thrown by the failed expect()
assertion.
By using a catch
block, we can capture the error
value thrown and pass it to done()
, which then displays it in the test output. Though not required, this is a best practice and will yield better test outputs.
Instructions
Let’s start by seeing how we are receiving a false positive.
Take a look at language_spoken.test.js where we’ve added a second test()
to test the functionality of the countryListLookup()
function. This function makes an asynchronous request to a REST API to get back a list of countries where a given language is spoken. The fetched result
is then passed to our provided callback.
If you look closely at the provided assertion being made, expect(undefined).toBeDefined()
, this test should fail, however, as we just learned, the test will provide a false positive!
Let’s begin by running the test command in the terminal to verify that our test is returning a false positive. The test should PASS (don’t use the done()
parameter yet!)
Now, let’s make sure that the test actually fails this time by rewriting the asynchronous test using the done()
callback as a parameter.
You should wrap the expect(undefined).toBeDefined()
assertion in a try
/catch
block and then call done()
at the appropriate time to notify Jest when to end the test.
To verify that you did this properly, run the npm test
command in the terminal. Your test should now fail.
Let’s now fix our testing function logic to verify that the function does indeed return something. Instead of expecting undefined
to be defined, assert that the result
value passed into our callback should be defined.
And finally, to verify that our function is working properly, run the test command in the terminal one last time. You should see that the test passes!