Test the Container Components
React Testing Library's approach to integration testing like a user would interact with the DOM works very well with React's function-based components.
Testing our functional components with React Hooks#
Now that we've gotten our app ready to run integration tests, it's time to write some integration tests.
Fair warning: this is going to be the longest lesson in this module, so get ready. If you can stick with it to the end, I think you'll feel a lot more confident about how to test these components.
In this lesson, I intend to walk you through writing tests for a couple of our container components relying on React Hooks.
Start with App.js (since tests are already begun...)#
Time to get to work on these tests. Since the
App.js file already has a single test, let's start there. We're going to completely overhaul how this file looks because this course is not only about modernizing enterprise React apps — it's also about showing you best practices for bigger codebases (and that includes how we structure our tests).
Describe your test file's various functionality#
The first thing I prefer to do when writing test files is to set up
describe blocks around each set of functionality within a test file.
If it helps, you can think of
describe as a wrapper in the test suite to divide up functionality — then each test inside of that
describe block will be a specific test for one particular piece of the functionality encompassed by the
describeto subdivide functionality
A good example of when
describecomes in handy might be for a file like our
ProductList.jsfile that has the ability to both display our products and also filter which products are displayed.
These two things are pretty different, so I might choose to break up those two pieces of functionality: displaying and filtering into two separate
describeblocks within my test file.
App.js file doesn't have that much functionality, but consistency within a codebase is also important, so to stay with the best practices, we'll give it a
If we look at the
App.test.js file now, it's basic. Go ahead and wrap the one test that's currently there within a
describe. Nothing fancy, either: just a barebones description of the file's purpose.
App.js is the root of our entire React project, I'll just put something like:
We're off to a good start.
The next thing we're going to do is purely optional, but it's more familiar to me and the way I've been writing software and tests, so I'm going to do it (even if just to show you how it's done). We'll change the
test syntax our first test started out with and switch it to use
test are pretty interchangeable, according to the Jest documentation, but I prefer the syntax of
it, which looks like:
it('should do this thing').
If you prefer
test('did not do other thing'), that's cool. You do you. Or whatever your team's already established as their preference: remember, consistency is important.
But since I'm writing this course, this is what I choose. So, our original test string goes from:
When to use
describeis best for breaking your test suite into smaller groups of tests. Depending on your test strategy, you might have a
describefor each function in your class, each module of your plugin, or each user-facing piece of functionality.
You can also nest
describes inside of one another to further subdivide the suite (I rarely do this myself, but it's possible).
itis for when we're writing the individual tests. We should be able to write each test like a little sentence, such as:
it('should show a product price when the item is rendered').
Make this test cover more#
Now it's time to beef up this test; it's a little skimpy on the details.
When we look at the homepage of Hardware Handler, we see a lot more than just the welcome message. We see links, we see buttons, and we see a checkout count of items if there are items present. Let's put those into this test to make sure they are visible when the app starts up.
As you can see from the beginning of this test, we declare a local variable called
title that is simply an element of text on the page. This is not required for the test to work, but it does make it easier to understand the element in the DOM we're targeting — especially if that element will come into play in more than one test assertion.
title, I'll declare a few new variables for the other elements I'm looking for on the page: the links to the products page, the add a new product page, and the checkout page.
title variable in the test, we'll add the following variables:
These three variables will now search the rendered page for these pieces of text.
Note that surrounding the particular strings you're looking for like
/some text onscreen/imakes the text search case insensitive (it will find all combinations of upper and lowercase versions of that text).
If you want it to be case sensitive, just wrap the text in quotes with whatever capitalization is expected: "Some other TEXT").
After defining these variables, we'll need to search for their presence on the page. These are a little different than our
title variable, though, because each text string is on the page twice: once in the nav bar and once on the main page.
Instead of searching for each text string to be in the document, we'll need to check that each text string has a length of two, indicating it's visible twice in the rendered components.
Here's the syntax we can use for that check:
Okay, now run the tests and let's see what happens.
In a terminal type:
And this is what we should see in the terminal after.
Nice! Our first test continues to pass, meaning all the text we expect to see is rendering when the app loads.
Write a second test to check on checkout count#
If we look at the code coverage for this
App.test.js file currently, it's decent, with an overall line coverage rating of almost 78%. As a rule of thumb, I want my integration test overall code coverage to be 80% or above to feel confident in my code.
Looking at the coverage report in the browser we can see the two functions bringing down our coverage percentage are the
We should be able to cover that
useEffect function that displays a number next to the Checkout link in the nav bar. Let's write a test for it.
Underneath our original test, create a new
it statement to check if there's a count present in the browser.
In order to display a count in the nav bar, we're going to need to mock our
useCheckout custom hook because that's what supplies any item counts to this component.
We can do that.
First, we'll import the
useCheckout Hook into this test component.
Spy on the hook and mock the results
Then, we'll use Jest's
spyOn function to spy on the the actual function we're calling (also named
useCheckout inside the hook) inside of our newly written test (this is why I imported the hook using the wildcard syntax
import * as useCheckout).
For mocks, I tend to use the name of the function I'm mocking and just stick the word
mock in front of it to indicate that whenever this function is called by the code being tested, this mock will be take over that function and return the data we'll define.
Where to set up mocks in test files
If this was a hook supplying essential data to make our component render, we'd need to declare this spy and mocked data in a
beforeEachblock before all the tests.
But since the app won't crash without any of that data (it just won't show a number next to the checkout icon in the nav bar), we'll just add the mock for the single test that really needs it in this file.
If we look at the data the real
useCheckout Hook returns, it gives us back an object of destructured properties: a
checkoutCount number, a
checkoutItems array of objects, a
setCheckoutItems function, and an
error boolean. We'll need to mock all those values within an item for this test to have the data it needs — with no items in the checkout, no number is rendered, and the test isn't doing its job.
useCheckout function already being spied upon, we can now define what it should return when the mock is called. Here's the test data I'll tell the mocked hook to return. This is also inside of our second test right after the declaration of the
Good. When our
<App /> component gets rendered now in the test that we're about to write, it should have all the data it needs to think there's a checkout item and display a count in the nav bar.
Write the test to check for a count in the DOM
Because we're mocking our
useCheckout Hook, which provides the state to our
<App /> component, we don't have to pass any props or set any other state before we render the component in the test.
Then, after rendering it, we'll look in the DOM for the number 1, which is what should be present now, provided our mocked hook is working. And that should be it.
So, here's what my new test code looks like:
Run our tests and recheck code coverage for
At this point, if you're not already running tests using the Jest CLI in watch mode (
yarn test) which tries to rerun them after every change to the files it's watching, let's go ahead and do that and check our new code coverage.
After our two tests run (and hopefully pass), our code coverage report in the terminal should now look like this.
With the addition of that second test, our
App.js file's code coverage has jumped up from 78% to just under 89%.
I'll take 89% code coverage for this file — the only bit of code still not covered now is when the
updateCheckoutCount function is called, but I think it will be easier to test that works in another file, so let's move on and test another component now.
Test our ProductList.js component#
That was a good warmup file to get our feet wet testing React components using hooks, and it's time to try out a component with some serious functionality in it. I'm thinking the
<ProductList> component would be a good one because of its ability to display and add products to the checkout, as well as its filtering capabilities.
Set up tests for the component#
Just like how the
<App> component had a
test/ folder inside of its
App/ folder in our project, we're going to do the same for this component.
In your IDE, open up the
ProductList/ folder and create a new folder inside of it called
test/. Inside of this folder, create a new test file named
ProductList.test.js, and we're ready to get started writing our next set of tests.
Describe the scenarios we want to test for ProductList#
With our test file created, it's time to set up our first
describe block for our tests, and we'll figure out what our actual
<ProductList> component needs in the way of data for it to render.
As with the tests for
App.js, we'll start off with the most basic
it statements here.
Mock out our custom hooks
When we examine the component we're testing, I see that it requires info from two of our custom hooks: the
useDepartments Hook and the
useProducts Hook. If either of those hooks' data is missing the component will throw errors, so we'll need to set the mocks for these hooks up before every test. Jest has just the thing for this:
Let's mock the
useDepartments Hook first in our test file. It will be the easier of the two.
At the top of our file, import the
useDepartments Hook (use the
* wildcard import so we can target the
useDepartments function within our mock), and right inside of our
describe block, create a
mockUseDepartments variable spying on the hook.
The code should look something like this: