Successfully Throwing Async Errors with the Jest Testing Library

The One Time Errors Should Happen in your Code

Paige Niedringhaus
Bits and Pieces

--

Photo by Science in HD on Unsplash

There’s only one situation developers want errors to happen, and that’s in specific tests

My development team at work jokes that bugs “are just features users don’t know they want yet”. 🤪

But as any good development team does, we try to prevent those bugs from happening to our users in the first place. We know that technical systems are not infallible: network requests fail, buttons are clicked multiple times, and users inevitably find that one edge case no one, not the developers, the product managers, the user experience designers and the QA testing team, even with all their powers combined, ever dreamed could happen.

We try to handle those errors gracefully so the application can continue to run, so our users can do what they came there to do and so we test: automated tests, manual tests, load tests, performance tests, smoke tests, chaos tests. Tests, tests, tests, tests, tests. 😫

While automated tests like unit and integration tests are considered standard best-practices, we still have a tendency, even during testing, to only cover the happy paths (the paths where all the API calls return, all the data exists, all the functions work as expected), and ignore the sad paths (the paths where outside services are down, where data doesn’t exist, where errors happen).

I’d argue, however, that those are the scenarios that need to be tested just as much if not more than when everything goes according to plan, because if our applications crash when errors happen, where does that leave our users? Up a creek without a paddle — or, more likely, leaving the app and going somewhere else to try and accomplish whatever task they set out to do.

Recently, I was working on a feature where a user could upload an Excel file to my team’s React application, our web app would parse through the file, validate its contents and then display back all valid data in an interactive table in the browser.

The catch, however, was that because it was an Excel file, we had a lot of validations to set up as guard rails to ensure the data was something our system could handle: we had to validate the products existed, validate the store numbers existed, validate the file headers were correct, and so on and so forth.

Instead of building all these validations into the React component with the JSX upload button, we made a plain JavaScript helper function (aptly named: validateUploadedFile()) that was imported into the component and it took care of most of the heavy lifting.

While it was very useful to separate out this business logic from the component responsible for initiating the upload, there were a lot of potential error scenarios to test for, and successfully verifying the correct errors were thrown during unit testing with Jest proved challenging. Contrary to what you might expect, there’s not a lot of examples or tutorials demonstrating how to expect asynchronous errors to happen (especially with code employing the newer ES6 async/await syntax).

But luckily, through trial and error and perseverance, I found the solution I needed, and I want to share it so you can test the correct errors are being thrown when they should be.

Today, I’ll discuss how to successfully test expected errors are thrown with the popular JavaScript testing library Jest, so you can rest easier knowing that even if the system encounters an error, the app won’t crash and your users will still be ok in the end.

Tip: Build Apps with reusable components, just like Lego

OSS Tools like Bit offer a new paradigm for building modern apps.

Instead of developing monolithic projects, you first build independent components. Then, you compose your components together to build as many applications as you like. This isn’t just a faster way to build, it’s also much more scalable and helps to standardize development.

Learn more

An independently source-controlled and shared “card” component. On the right => its dependency graph, auto-generated by Bit.

Jest Testing Framework

Jest, if you’re not as familiar with it, is a “delightful JavaScript testing framework.” It’s popular because it works with plain JavaScript and Node.js, all the major JS frameworks (React, Vue, Angular), TypeScript, and more, and is fairly easy to get set up in a JavaScript project.

While Jest is easy to get started with, its “focus on simplicity” is deceptive: jest caters to so many different needs that it offers almost too many ways to test, and while its documentation is extensive, it isn’t always easy for an average Jest user (like myself) to find the answer he/she needs in the copious amounts of examples present.

If you don’t believe me, just take a quick look at the docs on the site, and start scrolling down the left-hand nav bar — there’s a lot there! But enough about Jest in general, let’s get to the code I was trying to test, and the problem I needed to solve.

The Component to Test: ProductFileUpload.js

Below is a very, very simplified version of the React component I needed to unit test with Jest. I‘ll break down what its purpose is below the code screenshot.

ProductFileUpload.js

Code screenshot of a very simplified ProductUploadFile.js React component. The `handleUpload()` function is what calls the `validateUploadedFile()` helper function that performs the necessary business logic validations on the Excel file.

In a nutshell, the component allows a user to select an Excel file to upload into the system, and the handleUpload() function attached to the custom { UploadFile } component calls the asynchronous validateUploadedFile() helper function, which checks if the product numbers supplied are valid products, and if the store numbers provided alongside those products are valid stores. If all of the combinations are valid, the uploadErrors state remains an empty string and the invalidImportInfo state remains null, but if some combinations are invalid, both of these states are updated with the appropriate info, which then triggers messages to display in the browser alerting the user to the issues so they can take action to fix their mistakes before viewing the table generated by the valid data.

My mission now, was to unit test that when validateUploadedFile() threw an error due to some invalid import data, the setUploadError() function passed in was updated with the new error message and the setInvalidImportInfo() state was loaded with whatever errors were in the import file for users to see and fix.

The Jest Test Options

Before, I get to my final solution, let me talk briefly about what didn’t work. Because I went down a lot of Google rabbit holes and hope to help others avoid my wasted time.

If you just want to see the working test, skip ahead to the Jest Try/Catch example — that is the one that finally worked for me and my asynchronous helper function.

What Didn’t Work:

Jest MockResults()

The first thing I tried, which didn’t work, was to mock error results from the functions passed into the validateUploadedFile() function. Makes sense, right?

If, after the validateUploadedFile() function is called in the test, the setUploadedError() function is mocked to respond:

const setUploadError = jest.fn(() => new Error('some product/stores invalid'));

And the setInvalidImportInfo() function is called and returned with:

const setInvalidImportInfo = jest.fn(() => ({ 
invalidProducts: ['Product 123456'],
invalidStores: ['Store 123']
}));

According to the jest documentation, mocking bad results from the functions seemed like it should have worked, but it didn’t. Instead, every time I ran the test, it just threw the error message "upload error — some records were found invalid” (not the error message I was expecting) and failed the test.

Jest MockRejectedValue()

Next, I tried to mock a rejected value for the validateUploadedFile() function itself. This too, seemed like it should work, in theory.

But alas, this mock wasn’t successful either.

const mockValidateUploadedFile = jest.fn().mockRejectedValue('some product/stores invalid');

Once more, the error was thrown and the test failed because of it.

Jest SpyOn

I also gave Jest’s spies a try. I imported all the uploadHelper functions into the test file with a wildcard import, then set up a spy to watch when the validateUploadedFunction() was called, and after it was called, to throw the expected error.

const mockUploadError = jest.spyOn(uploadHelpers, 'validateUploadedFile')
.mockImplementation(() => Promise.reject(new Error('some product/stores invalid')));

Still no luck. By this point, I was really getting to the end of my rope — I couldn’t understand what I was doing wrong and StackOverflow didn’t seem to either.

But finally, a breakthrough came.

What Did Work:

Jest Try/Catch with Async/Await

After much trial and error and exclamations of “why doesn’t this work?!?!,” an answer was found, buried deep in Jest’s documentation among the Async Examples in the guides.

Here’s the working test code below.

ProductFileUpload.test.js

A Jest test that finally threw a successful error when `validateUploadedFile()` was called with invalid data supplied to it.

In the end, what actually worked for me, was wrapping the validateUploadedFile() test function inside a try/catch block (just like the original component’s code that called this helper function). The try/catch surrounding the code was the missing link.

Once I wrapped the validateUploadedFile() function, mocked the invalid data to be passed in in productRows, and mocked the valid data to judge productRows against (the storesService and productService functions), things fell into place.

The validation mocks were called, the setInvalidImportInfo() mock was called with the expectedInvalidInfo and the setUploadError() was called with the string expected when some import information was no good: "some product/stores invalid".

Finally! 🙌

I was then able to use this same test setup in numerous other tests in this file, testing other variations of the data that would result in different error messages and states to the users.

Problem solved.

Conclusion

Errors and bugs are a fact of life when it comes to software development, and tests help us anticipate and avoid at least some if not all of those errors but only when we actually take the time to test those sad path scenarios.

The JavaScript testing framework Jest offers many, many ways to handle tests just like this, and if we take the time to write them it may end up saving us a brutal, stressful debugging session sometime down the road when something’s gone wrong in production and its imperative to identify the problem and fix it. Even though writing test sometimes seems harder than writing the working code itself, do yourself and your development team a favor and do it anyway.

Check back in a few weeks — I’ll be writing more about JavaScript, React, ES6, or something else related to web development.

Thanks for reading. I hope this article gives you a better idea of a variety of ways to test asynchronous JavaScript functions with Jest, including error scenarios, because we all know, they’ll happen despite our best intentions.

References & Further Resources:

From monolithic to composable software with Bit

Split apps into components to make app development easier, and enjoy the best experience for the workflows you want:

Micro-Frontends

Design System

Code-Sharing and reuse

Monorepo

--

--

Staff Software Engineer at Blues, previously a digital marketer. Technical writer & speaker. Co-host of Front-end Fire & LogRocket podcasts