When unit testing a React application, you’ll want to test every aspect of it. This will include testing each of your contexts.

Keep in mind that creating a context often involves subscribing to something (like a service) and, it is being consumed (such as by components). Understanding that a context is similar to an environment in which a group of components exists is key. Therefore, our tests will need to mimic both behaviors—stubbing a service and consuming our context. Below, I’ll demonstrate just how to do that.

Consider the following context. It is a simple context that queries a list of people’s names. Those names are returned as an array of strings. (The API in my example isn’t anything fancy, but you could always do this with more complex objects.)

// This interface would likely reside in a separate file, but has been included here for brevity.
interface PeopleContextType {
  people: Array<string>;
}

const PeopleContext = createContext(PeopleContextType | null>(null);

interface Props {
  children?: ReactNode;
}

function PeopleProvider({ children }: Props) {
  const [people, setPeople] = useState<Array<string> | null>();
  
  useEffect(() => {
     ApiService.people().get().then((results) => {
         setPeople(results.data);
     });
  });

  const value: PeopleContextType = {
    people
  }

  return(
    <PeopleContext.Provider value={value}>{children}</PeopleContext.Provider>
  )
}

export { PeopleContext, PeopleProvider };

The provider is quite simple. It queries a service of people (the API service is a library of methods), and it receives an array of them in response. Once the response has been received, we update state with the array.

Of course, to use this context within your application, you would wrap your <App /> with it:

<PeopleProvider>
  <App />
</PeopleProvider>

To test the context is also quite simple. The key, as stated initially, is to understand what the context does, namely consuming a service and providing that information to our components. Therefore, to write our tests, we’ll need to do both.

Consider the following unit test. Admittedly, this test isn’t exhaustive as, in reality, we need to test for other conditions like no available service, the service is available but not returning data, and the service returning incorrect data. But, hopefully, the test below can at least help get you started.

const TestingComponent = () => {
  const context = useContext(PeopleContext);

  return (
    <ul>
      {context?.people &&
        context?.people?.map(person => (
          <li key={person}>
            {person}
          </li>
        ))
      }
    </ul>
  )
}

it("should render a list of people", async () => {
  const peopleResult: = {
      data: [
        "Foo",
        "Bar",
        "Baz"
      ]
  }

  const people = ApiService.people();
  const mockGet = jest.spyOn(people, "get");
  mockGet.mockResolveValue(peopleResult);

  render(
    <PeopleProvider>
      <TestingComponent />
    </PeopleProvider>
  );

  await waitFor(() => {
    const liElements = container.querySelectorAll("li");
    expect(liElements.length).toBeEqual(3);
    expect(liElements[0].textContent).toBeEqual("Foo");
    expect(liElements[1].textContent).toBeEqual("Bar");
    expect(liElements[2].textContent).toBeEqual("Buz");
  });
})

So, let’s take a look at what is involved in our test.

In order to test our context, we need to stub out a component that will reside within the context and use its data. This is where our TestComponent comes in. It will consume our context and display some of the data. The unit test will test the data being displayed. Keep in mind that it doesn’t matter how you display your data in this component. I’ve simply chosen to use an unordered list, and my tests will reflect that. You can choose a different approach if you choose, but you’ll need to update your test conditions to reflect your changes.

One thing to note before proceeding…my test component sits outside of my test fixture. This allows me to reuse my component for other test cases, such as those listed previously. You could choose to put this component in a setup fixture if you’d like, but given that you’re not establishing a scoped sandbox testing environment, it isn’t really necessary. You could also put this component in an external file. Whatever is fine. Personally, I place the test components at the top of my spec file.

Now for the test. Because this is a unit test and not an integration test, I don’t want my context actually calling my API service. Therefore, I begin by mocking some data that would be returned from my API. This data is the array of strings shown on lines 19-23. Next, I stub my API call and replace the call’s results with my mocked data (lines 26-28). Notice that on line 27, I’m stubbing the “get” method of my “people” service, which corresponds to lines 16-18 of my people context. The mocked data that I supply in my test is what is then saved to state (line 17 of the people context). This will be the data that is (hopefully) supplied to my test component.

We’ve just completed handling the first function of our context—subscribing to a service. Now, we need to mimic the second aspect of our context—consumption by a component. In order to do that, we wrap our test component with our context under test and render the result (lines 30-34). If all goes well, our fake data should be rendered correctly in our test component. It is time for us to test and make sure that is the case.

The final lines (36-42) demonstrate waiting for our test fixture’s lifecycle to complete (remember that the API call in our context is made within a useEffect hook and, therefore, the component will render a few times). You’ll see that after we attempt to query all list items within the document, we run a series of expectations against them. The first test checks to ensure that three list items were rendered. The final three expectations ensure that the right data (the mocked names from our service) is rendered.