In this article we will discuss how to test and mock React ContextAPI using Jest and React Testing Library (React Testing Library).
We will create a basic application and demo how to mock a contextAPI provider and consumer for a component unit test.
Our React application will be structured this way:
App (provider defined for children) --> ParentDisplay *(no provider defined for children) * --> ChildCompanyDisplay (consumer child)
Our challenge is this:
How do we conduct ParentDisplay
component unit testing to check whether values provided by contextAPI to child components are correctly rendered?
** The issue is that there is no provider defined in our ParentDisplay
component therefore its child component (ChildCompanyDisplay) will have nothing to consume (note: The provider is defined in our main parent App component)**
I had to solve the problem on my own.
Given that there are no specific resources addressing it, I decided to share my solution through a simple application.
We will solve this by using Jest to mock contextAPI.
So let's begin.
STEP 1: Create our app
Let's start first by creating our app.
yarn create react-app sample-test-app
cd sample-test-app
yarn start
STEP 2: Create our React ContextAPI Provider and Consumer
cd sample-test-app
mkdir contexts
cd contexts
touch CompanyContext.js
In the file insert the following code:
import React, { createContext, useState, useEffect } from "react";
const CompanyContext = createContext();
const CompanyProvider = ({ children }) => {
const [companyName, setCompanyName] = useState("The Data Life");
const [employee, setEmployee] = useState("Niccolo Lampa");
return (
<CompanyContext.Provider
value={{ companyName, employee }}
>
{children}
</CompanyContext.Provider>
);
};
const CompanyConsumer = (Child) => (props) => (
<CompanyContext.Consumer>
{(context) => <Child {...props} {...context} />}
</CompanyContext.Consumer>
);
export { CompanyProvider, CompanyConsumer };
The main purpose of the CompanyContext.js
is to define the provider and consumer for companyName
and employee
.
STEP 3: Create ChildCompanyDisplay Component
We will create a child component that will consume companyName
and employee
values from our contextAPI CompanyProvider
and display them.
import React from "react";
import { CompanyConsumer } from "../contexts/CompanyContext";
const ChildCompanyDisplay = ({ companyName, employee }) => {
return (
<div>{`${employee}: ${companyName}`}</div>
)
};
export default CompanyConsumer(ChildCompanyDisplay);
STEP 4: Create ParentDisplay
component
Let's create a parent for ChildCompanyDisplay
component.
The ParentDisplay
component will just function as a container.
cd src/components
touch ParentDisplay.js
import React from "react";
import ChildCompanyDisplay from "./ChildCompanyDisplay"
const ParentDisplay =() => {
return (
<div>
<ChildCompanyDisplay />
</div>
)
}
export default ParentDisplay
STEP 5: Add Company Provider and ParentDisplayComponent to App.js
Modify your App.js
to the following:
import { CompanyProvider } from "./contexts/CompanyContext";
import ParentDisplay from "./components/ParentDisplay"
import './App.css';
function App() {
return (
<div className="App">
<CompanyProvider>
<ParentDisplay />
</CompanyProvider>
</div>
);
}
export default App;
Mainly all the child components of App will have access to the CompanyProvider
companyName
and employee
.
Therefore our simple application will look like this.
STEP 6: Modify our App test suite
Now let's start testing.
Let's create a test for App first as a reference for our succeeding test.
import { render, screen, cleanup } from '@testing-library/react';
import App from './App';
describe("App Test", () => {
afterEach(() => {
cleanup();
});
test('App displays employee name properly', () => {
render(<App />);
const employeeName = screen.getByText(/Niccolo Lampa/i);
expect(employeeName).toBeInTheDocument();
});
test('App displays company name properly', () => {
render(<App />);
const companyName = screen.getByText(/The Data Life/i);
expect(companyName).toBeInTheDocument();
});
})
Next let's run our app test.
yarn test
The result is that our App
passes the two tests, meaning that our child components are able to receive the companyName
and employee
given by our contextAPI CompanyProvider
. So no problem there.
STEP 7: Create ParentDisplay unit tests
Now let's create our unit test for our ParentDisplay
component. It will be really similar to our App.test.js
, the only difference is that we are rendering <ParentDisplay />
.
cd src/components
touch ParentDisplay.test.js
import React from "react";
import {
render,
screen,
cleanup
} from "@testing-library/react";
import ParentDisplay from "./ParentDisplay"
describe("ParentDisplay Unit Test", () => {
afterEach(() => {
cleanup();
});
test('Parent displays employee name properly', () => {
render(<ParentDisplay />);
const employeeName = screen.getByText(/Niccolo Lampa/i);
expect(employeeName).toBeInTheDocument();
});
test('Parent displays company name properly', () => {
render(<ParentDisplay />);
const companyName = screen.getByText(/The Data Life/i);
expect(companyName).toBeInTheDocument();
});
});
Let's run the tests again including ParentDisplay
test.
yarn test
As you can see the two tests are failing. For some reason the ParentDisplay can't render the elements properly the companyName
and employee
are showing up as undefined
.
Why is it working for App.test.js
and not for our ParentDisplay.test.js
?
This is related to the main challenge mentioned in our intro section.
Remember that it is only App.js
that defined a contextAPI provider CompanyProvider
. So all children of App.js are able to access this provider.
However, ParentDisplay
component does not define contextAPI provider CompanyProvider
for its children. Therefore its child components (e.g. ChildCompanyDisplay) are not able to consume companyName
and employee
variables from CompanyProvider
.
This is the main reason why tests for the ParentDisplay
component are failing.
Again below is the hierarchy of parent-children for reference.
App (provider defined for children) --> ParentDisplay *(no provider defined for children) * --> ChildCompanyDisplay (consumer child)
So what is the solution?
You may be tempted to just wrap our ParentDisplay
component with the CompanyProvider
<CompanyProvider>
<ParentDisplay />
</CompanyProvider>
Although for our simple application it may result to our tests passing, this is not the most ideal solution. This solution will create testing issues and likely fail and incompatible for more complex applications (based on my experience).
STEP 9: Mock our contextAPI provider and consumer with Jest.
For more complex apps, you will need to mock the context API and provider and consumer.
For example, you may need to mock provider when companyName
and employee
variables are provided by a third-party API (e.g. authentication API) rather than being a constant (changing values based on login information). In this case you will mock the contextAPI provider to provide fixed values for testing.
So this is where Jest comes in.
We will create a mocks for our contextAPI provider and consumer using Jest.
cd src
mkdir __mocks__
touch CompanyContextMock.js
import React, { createContext } from "react";
// NOTE for jest mocking to work and access these out-of-scope variables. Variable names must be prefixed with "mock"
const MockCompanyContext = createContext();
const companyName= "The Data Life"
const employee= "Niccolo Lampa"
const MockCompanyProvider = ({ children }) => (
<MockCompanyContext.Provider
value={{
companyName,
employee
}}
>
{children}
</MockCompanyContext.Provider>
);
const MockCompanyConsumer = (Child) => (props) => (
<MockCompanyContext.Consumer>
{(context) => <Child {...props} {...context} />}
</MockCompanyContext.Consumer>
);
export { MockCompanyProvider, MockCompanyConsumer };
Remember to prefix the variables with mock
for Jest to access these out of scope variables. If not you will see the following error:
The module factory of
jest.mock()
is not allowed to reference any out-of-scope variables ... Note: This is a precaution to guard against uninitialized mock variables. If it is ensured that the mock is required lazily, variable names prefixed withmock
(case insensitive) are permitted.
STEP 9: Revise our ParentDisplay test
We will employ these contextAPI mocks by updating ParentDisplay.test.js
import React from "react";
import {
render,
screen,
cleanup
} from "@testing-library/react";
// UPDATE
// mocks import
import {
MockCompanyConsumer,
MockCompanyProvider,
} from "../__mocks__/CompanyContextMock.js";
import ParentDisplay from "./ParentDisplay"
// mocking companyContext's companyConsumer module for child components.
jest.mock("../contexts/CompanyContext", () => ({
...jest.requireActual("../contexts/CompanyContext"),
CompanyConsumer: MockCompanyConsumer,
}));
describe("ParentDisplay Unit Test", () => {
afterEach(() => {
cleanup();
});
// wrap our ParentDisplay component with MockCompanyProvider
test('Parent displays employee name properly', () => {
render(
<MockCompanyProvider>
<ParentDisplay />
</MockCompanyProvider>
);
const employeeName = screen.getByText(/Niccolo Lampa/i);
expect(employeeName).toBeInTheDocument();
});
test('Parent displays company name properly', () => {
render(
<MockCompanyProvider>
<ParentDisplay />
</MockCompanyProvider>
);
const companyName = screen.getByText(/The Data Life/i);
expect(companyName).toBeInTheDocument();
});
});
These code block in our ParentDisplay.test.js
:
jest.mock("../contexts/CompanyContext", () => ({ ...jest.requireActual("../contexts/CompanyContext"), CompanyConsumer: MockCompanyConsumer, }));
will replace all imports of CompanyConsumer
from the CompanyContext
module with our MockCompanyConsumer
in all the children of ParentDisplay
.
Now all of our tests from App.test.js
and ParentDisplay.test.js
are now passing.
You can clone the full repository via my Github repositiory:
https://github.com/niccololampa/unit-test-mock-react-context-api-demo
Until next time. Keep learning.
Stay stoked and code. :)
I hope you can voluntarily Buy Me A Coffee if you found this article useful and give additional support for me to continue sharing more content for the community. :)
Thank you very much. :)