If you want to ship your applications with confidence—and of course you do—you need an excellent suite of automated tests to make absolutely sure that when changes reach your users, nothing gets broken. To get this confidence, your tests need to realistically mimic how users actually use your React components. Otherwise, tests could pass when the application is broken in the real world.
In this course, we’ll write a series of render methods and run a range of tests to see how we can get the confidence we’re looking for, without giving up maintainability or test run-speed.
Let’s start basic by using ReactDOM to render a simple React component to a <div> we create ourselves and assert that it’s rendering the right thing based on the props we provide.
The Jest DOM library provides really useful extensions to jest’s built-in assertion library that will make it easier for us to write our test assertions (like toHaveTextContent). Let’s see how to use that in our project.
Our tests are currently tightly coupled to the implementation of our component’s element structure. A refactor to that structure wont break our application, but will definitely break our tests because we’re testing implementation details. Let’s use DOM Testing Library to create a custom render function that will give us some helpful utilities for searching for elements in the DOM in the same way a user would.
Let’s create a simple render method to be able to reuse this functionality for our other tests. The render method we’ve created is similar to the render method that’s provided by React Testing Library. Let’s swap our implementation to that!
While you’re writing your tests it can be helpful to see what the DOM looks like. You can do this with React Testing Library’s debug function which will log a formatted and syntax highlighted state of the DOM at the time it is called.
The fireEvent utility in React Testing Library supports all the events that you regularly use in the web (change, click, etc.). Let’s see how we can test our change event handler with an input.
The User Event module is part of the Testing Library family of tools and lets you fire events on DOM nodes that more closely resemble the way your users will interact with your elements. Let’s refactor our fire event usages to use that instead.
Sometimes it can be useful to change the props of a component you’ve rendered and make assertions about what’s rendered after that prop change has taken place. Let’s see how to go about doing this with React Testing Library.
It’s pretty straightforward to assert that a certain element is rendered with react-testing-library, but what if we want to ensure that something is NOT being rendered. For example, if we re-render our component with a different maximum amount leading to the error message being hidden. Let’s see how we can use the query* APIs from react-testing-library to assert that certain elements are not rendered.
If you have a component that makes HTTP requests, you’ll probably want to mock those out for UI unit and integration tests. Let’s see how to use jest’s built-in mocking capabilities to do that.
Mocking an API module works, but it's incomplete because we have to make all these assertions that we're calling it properly and then we'd need to write some other tests for the API module itself to make sure that it responds properly to those calls.
In this lesson, I'll show you how you can use a package called MSW to intercept and handle those HTTP requests instead so we can get a great deal more confidence.
There are some situations where you want to focus your tests on a particular component and not any of its children. There are other situations where you want to mock out the behavior of components that your component renders. In the case of react-transition-group, we don’t want to have to wait until the transition has completed before we can go on with our tests. Let’s see how we can mock the implementation of react-transition-group using jest.mock to make our tests more reliable and easier to write and maintain.
If the user experiences an error while using your application you may want to send information to a monitoring tool to ensure you can be made aware of the error and deal with it as quickly as possible. Let’s see about testing a component that is responsible for this componentDidCatch error boundary using React Testing Library.
When testing an error boundary, your console will be filled with console.error calls from React. Those can be a real distraction from the rest of the output for your tests. Let’s clean those up with jest.spyOn.
Our error boundary has some other use cases that it supports and we should try to make sure our tests cover all those use cases, so let’s add a test to make sure the recovery feature of our error boundary works properly.
We have a bit of repetition in our rerender calls here and it would be nice if we could avoid that. Let’s use the wrapper option for React Testing Library so we can avoid the repetition in our rerender calls.
Normally using Test Driven Development (TDD) with UI is really difficult because testing utilities for UI often tie your tests closely to the implementation. Because React Testing Library is not this way, we can actually use it to TDD our UI. Let’s build the structure of a form using TDD with React Testing Library.
Now that we have our react form structure written using TDD, let’s take it a step further and TDD our form’s submission with an onSubmit handler as well.
Once the data has been saved on the server, we’ll want to redirect the user to the home screen with react-router’s <Redirect /> component. Let’s go ahead and mock that component as well and verify that it’s being rendered with the correct props. We’ll have to make our test asynchronous so we can make that assertion because the <Redirect /> component is rendered after the async call happens.
It’s great that our form can be submitted, but we need to get that data to our server to actually save the post to the database. We don’t want to actually make requests to the server during these unit tests, so we’ll mock out the function responsible for doing that and assert that it’s being called with the right data. Then we’ll go ahead and implement that functionality in our form component.
Our post should probably have a creation date associated with it (typically you’ll add this on the server, but for the purposes of our example, we’ll do it on the client). Dates are notoriously challenging to test. There are existing libraries that help, but often you can get close enough to the desired effect for your assertions. Let’s go ahead and test that a correct creation date has been set for our posts.
A really important aspect of TDD is the refactor phase. A critical piece to making your tests easier to maintain is using code structure and values to communicate what is important and what is not. We’ll use data generation with test-data-bot to communicate that the specific data values are irrelevant and it’s just what kind of data it is. Let’s refactor our tests to communicate this effectively.
We have the happy path covered for our post editor component, but what happens if there’s an error in saving the user’s information? We should probably show them an error message and give them the chance to try again. Let’s add a new test for this error case and implement some error handling.
We’ve finished with our post editor component, but let’s not forget the refactor phase! We have a bunch of duplicate logic between our two tests. When you have multiple tests with duplicate logic, it becomes hard for folks who come to maintain the tests later to identify what the differences between the tests are. This makes it harder to understand what the different features of the component are. So let’s refactor the tests slightly so we can communicate clearly what the important and unique parts of the tests are to make it easier for future maintainers.
Mocking the <Redirect /> component in react-router works, but it’s imperfect because we don’t know for sure that the user will be redirected properly. Alternatively, we can render our component within a Router with a custom implementation of a history via createMemoryHistory. Then we can make assertions on that history object.
If the user enters a bad URL or there’s some kind of routing error, we should show the user a helpful error page. Let’s see how we can test that our page is wired up with the router properly to do this.
Many of our components need (or will eventually need) the router context from the router provider. Let’s write a custom render method that looks and acts like the render method from React Testing Library but adds features for our router so we don’t have to concern ourselves about the implementation detail of the router.
Similar to react-router, we can render our component under test with the redux provider, our full redux store, and then we can interact with our component in the same way the user would and be confident that our component, action creators, and reducers are all working properly.
There are definitely some scenarios where you want to test what a component will do given certain state in the store. Let’s see how we can initialize our redux provider with a store that has some custom initialized data. One thing that I should note here is that while it can be useful to do this, make sure you at least have one test that verifies that you can into this state in the first place.
It’s very common for our components to use state from the redux store (or to be refactored to use it eventually). Having to change our tests every time we do this would be frustrating. Let’s see how we can write a render method we could use to not worry about that implementation detail when it’s irrelevant for our tests. This could be combined with the custom render method for the router to be a single render method that supports all providers our components need.
We’ve got a custom useCounter hook here and we want to make sure the increment and decrement functions it returns will update the count state that it returns. Because hooks must be run within the render phase of a component, we’ll create a simple component that simply uses the hook and render that component. One catch is that whenever there’s a state update in your tests, React needs you to wrap that in a call to act. We don’t have to do that for regular component tests that use React Testing Library utilities because those utilities manage the act call internally, but since we’re calling these state updater functions ourselves, we have to wrap things in act. Let’s do that for our useCounter hook.
Once we add tests for all the use cases of our hook, we’ll notice a bunch of common setup, so let’s put that in a setup function.
That setup function is pretty handy. Seems like a good opportunity for an abstraction. Well, we already have one! It’s called React Hooks Testing Library. Let’s swap our setup function for the renderHook function from @testing-library/react-hooks.
Just like React Testing Library, React Hooks Testing Library has a rerender function which can be useful for testing changes to props. Let’s test what happens if the step option changes for our hook.
When you use a React Portal, much of your component can be rendered outside the main DOM tree. Let’s see how we can use utilities provided by React Testing Library to allow us to select elements within the portal. To perform actions and assert on what’s rendered.
Sometimes your react component may need to do some cleanup work in the return value from useEffect or useLayoutEffect, or the componentWillUnmount lifecycle method for class components. Luckily for us, as far as React Testing Library is concerned, whichever of those you use is an implementation detail, so your test looks exactly the same regardless of how you implement it. Check out how you can test that simply with React Testing Library and a <Countdown /> component.
You can get a huge amount of confidence and coverage from integration tests that test an entire page, or even your entire app. Let’s write a test that renders our whole app using React Testing Library and navigate around it like a normal user would. These tests are typically a bit longer, but they provide a huge amount of value.
By using some of the get queries, we’re assuming that those elements will be available on the page right when we execute the query. This is a bit of an implementation detail and it’d be cool if we could not make that assumption in our test. Let’s swap all those for find queries.
Let’s take things a step closer to the way our app is used by using the User Event module for interacting with our app. Once we’ve done all this implementation detail-free testing, we’ll find out that we can drastically change the implementation of our app and our tests can give us confidence that our changes were successful. That’s fantastic!