Niccolo Lampa
The Data Life

The Data Life

Testing and Mocking React ContextAPI With Jest and React Testing Library

Testing and Mocking React ContextAPI With Jest and React Testing Library
Niccolo Lampa's photo
Niccolo Lampa

Published on Jan 2, 2022

7 min read

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.

app-display-company.png

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.

app-test.png

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

failed-parent-unit-tests.png

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 with mock (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.

tests-passing.png

You can clone the full repository via my Github repositiory:

https://github.com/niccololampa/unit-test-mock-react-context-api-demo

Until next time.

Stay stoked and code.

For more information see the official React's Context API documentation

 
Share this