How To Write Tests For React 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.
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 functionalityA good example of when
describe
comes in handy might be for a file like ourProductList.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:
xxxxxxxxxx
describe('Hardware handler app', () => {
test('renders Hardware Handler without crashing', () => {
render(<App />);
const title = screen.getByText(/welcome to hardware handler/i);
expect(title).toBeInTheDocument();
});
});
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:
xxxxxxxxxx
test('renders Hardware Handler without crashing', () => {
to:
xxxxxxxxxx
describe('Hardware handler app', () => {
it('should render app without crashing', () => {
When to use
describe
versusit
in tests
describe
is best for breaking your test suite into smaller groups of tests. Depending on your test strategy, you might have adescribe
for each function in your class, each module of your plugin, or each user-facing piece of functionality.You can also nest
describe
s 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:
xxxxxxxxxx
const title = screen.getByText(/welcome to hardware handler/i);
const productButton = screen.getAllByText(/my products/i);
const newProductButton = screen.getAllByText(/add new products/i);
const checkoutButton = screen.getAllByText(/checkout/i);
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:
xxxxxxxxxx
const checkoutButton = screen.getAllByText(/checkout/i);
expect(title).toBeInTheDocument();
expect(productButton).toHaveLength(2);
expect(newProductButton).toHaveLength(2);
expect(checkoutButton).toHaveLength(2);
});
Okay, now run the tests and let's see what happens.
In a terminal type:
xxxxxxxxxx
cd client/ && yarn test
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 useEffect
and updateCheckoutCount

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.
xxxxxxxxxx
it('should display checkout count in nav bar when there are items in checkout', () => {
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.
xxxxxxxxxx
import * as useCheckout from '../../../hooks/useCheckout';
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
).
xxxxxxxxxx
it('should display checkout count in nav bar when there are items in checkout', () => {
const mockUseCheckout = jest.spyOn(useCheckout, '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.
xxxxxxxxxx
const mockUseCheckout = jest.spyOn(useCheckout, 'useCheckout');
​
mockUseCheckout.mockReturnValue({
checkoutCount: 1,
checkoutItems: [
{
brand: 'Test Brand',
departmentId: 1,
description: 'My test description',
id: 1,
name: 'A test name',
productId: 1,
quantity: 1,
retailPrice: 1234,
},
],
setCheckoutItems: jest.fn(),
error: false,
});
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:
xxxxxxxxxx
render(<App />);
const checkoutCount = screen.getByText(/1/i);
expect(checkoutCount).toBeInTheDocument();
});
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.
xxxxxxxxxx
cd client/ && yarn 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 describe
and it
statements here.
xxxxxxxxxx
describe('Product List component', () => {
​
it('should render the component without crashing', () => {}
}
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:
xxxxxxxxxx
import * as useDepartments from '../../../hooks/useDepartments';
​
describe('Product List component', () => {
const mockUseDepartments = jest.spyOn(useDepartments, 'useDepartments');
This lesson preview is part of the The newline Guide to Modernizing an Enterprise React App course and can be unlocked immediately with a \newline Pro subscription or a single-time purchase. Already have access to this course? Log in here.
Get unlimited access to The newline Guide to Modernizing an Enterprise React App, plus 70+ \newline books, guides and courses with the \newline Pro subscription.
