This video is available to students only

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 describe.

Use describe to subdivide functionality

A good example of when describe comes in handy might be for a file like our ProductList.js file 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 describe blocks within my test file.

Our 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 describe anyway.

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.

Since App.js is the root of our entire React project, I'll just put something like:

We're off to a good start.

Switch from test to it syntax#

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 it instead.

it and 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:

to:

When to use describe versus it in tests

describe is best for breaking your test suite into smaller groups of tests. Depending on your test strategy, you might have a describe for 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).

it is 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.

Just like 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.

Underneath the 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/i makes 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.

First passing test for the App.test.js file

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 useEffect and updateCheckoutCount

Initial code coverage report of App.js in the browser

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.

Import useCheckout

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 beforeEach block 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.

With our 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 mockUseCheckout variable.

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 App.js#

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.

Newly updated terminal output of code coverage for App.js file

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 describe and 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: beforeEach.

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:

Start a new discussion. All notification go to the author.