How to test React Context

3 min read

React Context is a tool for designing flexible Component APIs. Let's explore how to write unit tests for components that use it.

This is an example of implementation testing. Which is not ideal, but it might be worth testing anyway to give you confidence in your code.

Theming Example

Instead of "prop drilling" where we pass a theme prop into every component, we can create a ThemeContext which will be consumed by many components:

import { createContext } from "react";

export const ThemeContext = createContext({
  theme: "light",
  onThemeChange: () => {}
});

The source of truth for the active theme is the theme property, and the onThemeChange function allows any component to change the theme.

To get any value from ThemeContext we need to wrap any components that need to access the theme:

import React, { useState } from "react";
import BlogPost from "./BlogPost";
import { ThemeContext } from "./ThemeContext";

export default function Page() {
  const [theme, setTheme] = useState("light");

  return (
    <ThemeContext.Provider
      value={{
        theme,
        onThemeChange: (newTheme) => setTheme(newTheme),
      }}
    >
      <BlogPost content="This is blog content" />
    </ThemeContext.Provider>
  );
}

We can then make use of the ThemeContext in the <BlogPost /> component. It both reads the theme value and updates it through the onThemeChanged callback:

import React from "react";
import { useContext } from "react";
import { ThemeContext } from "./ThemeContext";
import { getActiveClasses } from "get-active-classes";
import "./BlogPost.css";

export default function BlogPost({ content }) {
  const { theme, onThemeChange } = useContext(ThemeContext);

  return (
    <article
      // Should we test this?
      className={getActiveClasses({
        "light-theme": theme === "light",
        "dark-theme": theme === "dark",
      })}
    >
      {content}
      <button
        onClick={() => onThemeChange(theme === "dark" ? "light" : "dark")}
      >
        Toggle Theme
      </button>
    </article>
  );
}

Testing The Consumer

Components can either be a Consumer or Provider of context, not both. <BlogPost /> is a Consumer, lets write tests for it:

import React from "react";
import BlogPost from "./BlogPost";
import { render, screen } from "@testing-library/react";
import { ThemeContext } from "./ThemeContext";
import userEvent from "@testing-library/user-event";

describe("<BlogPost />", () => {
  describe("when theme is dark", () => {
    const theme = {
      theme: "dark",
      onThemeChange: jest.fn(),
    };

    beforeEach(() => {
      render(
        <ThemeContext.Provider value={theme}>
          <BlogPost />
        </ThemeContext.Provider>
      );
    });

    describe("when clicking toggle", () => {
      beforeEach(() => userEvent.click(screen.getByText(/toggle theme/i)));

      it("theme callback is ran", () => {
        expect(theme.onThemeChange).toHaveBeenCalledWith("light");
      });
    });
  });
});

Notice here we don't have 100% code coverage of <BlogPost />, we are missing tests for the dynamic classes: .light-theme and dark-theme. Testing styles is best left to tools like Storybook.

Testing The Provider

If the Provider component does not have { children } props like <Page />, we can employ a speciality tool: mocking a child component and consume the Context we want to test.

This is a very fragile test as it relies a heavily on the components implementation. A false-negative test could happen if the mocked component is removed from the <Page /> component.

With that warning, here is an example of the speciality tool:

import React, { useContext } from "react";
import Page from "./Page";
import { render, act } from "@testing-library/react";
import { ThemeContext } from "./ThemeContext";
import BlogPost from "./BlogPost";

jest.mock("./BlogPost", () => jest.fn());

describe("<Page />", () => {
  let theme, onThemeChange;
  beforeEach(() => {
    BlogPost.mockImplementation(() => {
      let themeContext = useContext(ThemeContext);
      theme = themeContext.theme;
      onThemeChange = themeContext.onThemeChange;
      return <div>Blog Post</div>;
    });
    render(<Page />);
  });

  describe("when page is initialized", () => {
    it("it provides light theme as default", () => {
      expect(theme).toBe("light");
    });
  });

  describe("when theme change callback is called", () => {
    beforeEach(() => {
      // act() needed because this callback triggers a React render
      act(() => onThemeChange("dark"));
    });

    it("it provides the new theme value", () => {
      expect(theme).toBe("dark");
    });
  });
});

The BlogPost.mockImplementation() is the important and dangerous aspect. We mock a child component of <Page /> and consume the context which we can run assertions against.

Was this article helpful?

I'm writing an ebook called Robust UI. Subscribe to get chapters from it via email