DEV Community

Cover image for Testing React Applications That Use Context Global State
Rufat Aliyev
Rufat Aliyev

Posted on • Edited on

Testing React Applications That Use Context Global State

Isn't satisfying to see all your test are passing with all those green tags in your terminal. I want to share the way I test my React applications that use Context to manage global state.

If you want to read how I use Context for global state management check out this post.

General guidelines

โ€œProgram to an interface, not an implementation.โ€
Design Patterns: Elements of Reusable Object Oriented Software

While my internship at Acerta we needed to set up our testing environment and I was assigned to research current testing approaches. As a result, I found two main streams in testing React applications.

  • Implementation oriented
  • Result oriented

Implementation oriented

If you are trying to test the internals of your component, like, is the state updating correctly, is the rendering happening, then you are doing implementation-oriented testing. The problem with this approach is that

  • your implementation could change while still rendering the same interface and functionalities. It means that every time you make changes to your component you will need to adjust your tests as well, which is not ideal.
  • you will need to have more tests and mocks.

Maybe you think that there are times that some logic needs to be thoroughly tested. In that case, your logic is too complex to be hosted by a React component. Create a custom hook and implement your logic there and import them to your component. This will make your component lighter and your testing easier.

Result oriented

Testing the result of your component is when you are testing your components closer to the way your users will interact with them. It means that you are not testing React rendered objects, but the real DOM. In this way, you will also test if your component is rendered at all, and if the elements that carry the main logic of the component are in the DOM, and if they behave correctly. The benefits of this approach is that

  • you will have more robust tests that will be subject to less often changes
  • you will test more with less code
  • you will test in a way as your application will be interacted by users

Mock API Requests

Enough of philosophy, let's get started with coding.

I usually use msw to mock my API requests. I strongly recommend it for your development environment. MSW is using service workers to intercept your API requests, which means that you don't change the way you fetch data. Just your API responses will come not from a server but predefined handlers.

It is very useful while doing testing. Because you can use it in the browser and node environment.

Mock Global State

Now that we are good with API requests, let's tackle the global state.

As my components are using the global state directly, I need to mock it so that I can assert if the methods provided by the global state are being called properly.

I start by mocking my store object and assign Jest mock functions to all the methods that will be imported by the components I will test.

export const store: DefaultContext = {
  getRandomRecipes: jest.fn(),
  getRecipeById: jest.fn(),
  searchByName: jest.fn(),
  searchByCountry: jest.fn(),
  searchByCategory: jest.fn(),
  searchByIngredients: jest.fn(),
  resetReviewState: jest.fn(),
  setRecipeList: jest.fn(),
  loading: false,
  recipeList: null,
  reviewBarOpen: false,
  reviewLoading: false,
  reviewedRecipe: null,
};
Enter fullscreen mode Exit fullscreen mode

The next step will be creating a mock <StateProvider/>.

import React from "react";
import { Context } from "@/store/Context";
import { store } from "./mockStore";

export const StateProvider: React.FC = ({ children }) => {
  return <Context.Provider value={store}>{children}</Context.Provider>;
};
Enter fullscreen mode Exit fullscreen mode

As you see here, I use the same Context element, but I pass my mock store to it as a value.

Alright, and now let's finally do some testing.

So the main technologies I use for testing are Jest and Testing-library.

Testing-library especially is created to encourage result-oriented testing. It provides you with utilities to render your component and deal with asynchronous methods in your components. It also provides screen API which represents the rendered element and selectors like getByText, getByTestId and etc.

I want to specially talk about getByTestId. You can get elements from DOM in numerous ways and most cases, it can be valid. But if you think about making your tests more resilient to changes you don't want them to be dependant on tag decisions someone made or alt text or text content, and so on. These are the things that can be changed more often and sometimes you can't do even anything about it. That's why I recommend using the data-testid property on your HTML tags.

  • One benefit is that no matter what you render as long as you have data-testid on it your test will pass.
  • Another advantage is that it will communicate to other developers that this particular element is linked to some tests.

Let's write some tests

I am going to test <ByCategory/> component from my Recippy project. This component is responsible for fetching categories from the server, displaying tabs with categories, and searching recipes by the selected category. It looks like this.

bycategory

So I will mimic this in my test.

First, I start up my mock server. (msw)

describe("ByName", () => {
  server.listen();
Enter fullscreen mode Exit fullscreen mode

Then I pick the method I want to run my assertion on. I use Jest spyOn method to reference the searchByCategorymethod in the global state.

  it("Should search by category", () => {
    const spy = jest.spyOn(mockStore, "searchByCategory");
Enter fullscreen mode Exit fullscreen mode

Wrapping my element with my mock global state ...

    render(
        <StateProvider>
          <ByCategory />
        </StateProvider>
      );
Enter fullscreen mode Exit fullscreen mode

Waiting for the loader to be unmounted. . .

    await waitForElementToBeRemoved(() => screen.getByTestId(LOADER));
Enter fullscreen mode Exit fullscreen mode

Selecting a tab . . .

   const tab = screen.getByTestId(CATEGORY + index);

   expect(tab.textContent).toBe(categoryNames[index].strCategory);

   fireEvent.click(tab);
Enter fullscreen mode Exit fullscreen mode

Submitting the search . . .

    const search_btn = screen.getByTestId(SEARCH_BTN);

    fireEvent.click(search_btn);
Enter fullscreen mode Exit fullscreen mode

Asserting if my searchByCategory method is called properly . . .

   expect(spy).toBeCalledTimes(1);
  });
Enter fullscreen mode Exit fullscreen mode

Finally, I close the server.

  server.close();
});
Enter fullscreen mode Exit fullscreen mode

That's it. Nothing fancy.

As you can see, I do the same thing as the user would do, but I test lots of things there. I test if I got a result from API if my loader was there and disappeared after the request is finalized if I had tabs to click, and finally if I can call API again to get my search result.

As you can see this test covers most parts of the component. Of course, you can test use cases as well, but this is the case I am interested in.

Finally, remember that tests are dangerous if not implemented correctly.

You wanna know more about testing and Javascript development I would highly recommend following [Kent C. Dodds].

Thanks for reading.

Top comments (0)