2

I've searched high and low for best practices when using testing react library. I have a test which uses react context which I'm trying to test with a component to ensure it updates correct. It's quite a bit of code so I'll narrow it down here (though still a lot).

Working version of this code can be found here https://codesandbox.io/s/react-playground-forked-mllwv8 (though the test platform isn't set up so this may not help much as not sure how to implement webpack on cs)

The run of functionality is:

  • PeoplePageComponent loads
  • useEffect runs and runs fetchPeople from the CONTEXT
  • fetchPeople(context function) runs fetchPeople(actual function) from fetchPeople.jsx which dispatches SET_PEOPLE
  • in PeoplePageComponent useEffect is triggered to where logic setsCurrentPage

I want to test:

  • render PeoplePageComponent with providerValue (people: null)
  • Assert page is loading
  • assert mockFetchPeople was called 1 time
  • Rerender PeoplePageComponent with providerValue (people:[{name: "tommy", age: 24}]) (THIS IS THE BIT I'M UNSURE OF)
  • assert mockSetCurrentPage was called 1 time
  • assert page is hasPeople

What would be the best way to do this given the set up I have? please bare in mind this code is actually very different to the code I'm using but the mechanisms are used in the same way

fetchPeople.jsx

export const fetchPeople = (dispatch) => {
  // faking a request here for the sake of demonstration
  const people = [
    {
      name: "Tommy",
      age: 24
    }
  ];
  setTimeout(() => {
    dispatch({
      type: "SET_PEOPLE",
      payload: people
    });
  }, 5000);
};

PeoplePageComponent.jsx

import React, { useContext, useEffect } from "react";
import { MyContext } from "./MyProvider";

export const PeoplePageComponent = () => {
  const { currentPage, people, setPage, fetchPeople } = useContext(MyContext);

  useEffect(() => {
    if (!people) return;
    setPage(people.length > 0 ? "hasPeople" : "noPeople");
  }, [people]);

  useEffect(() => {
    fetchPeople();
  }, []);

  return (
    <>
      <p>
        <b>Page:</b> {currentPage}
      </p>

      <p>
        <b>People:</b> {JSON.stringify(people)}
      </p>
      {currentPage === "loading" && <h1>This is the loading page</h1>}
      {currentPage === "noPeople" && <h1>This is the noPeople page</h1>}
      {currentPage === "hasPeople" && <h1>This is the hasPeople page</h1>}
    </>
  );
};

MyProvider.jsx

import { createContext, useReducer } from "react";
import { fetchPeople } from "./fetchPeople";

const reducer = (state, action) => {
  switch (action.type) {
    case "SET_PAGE":
      return {
        ...state,
        currentPage: action.payload
      };
    case "SET_PEOPLE":
      return {
        ...state,
        people: action.payload
      };
    default:
      return state;
  }
};

const initialState = {
  currentPage: "loading",
  people: null
};

export const MyContext = createContext({
  setPage: () => {},
  setPeople: () => {},
  fetchPeople: () => {},
  currentPage: "loading",
  people: null
});

export const MyProvider = ({ children }) => {
  const [state, dispatch] = useReducer(reducer, initialState);

  const providerValue = {
    setPage: (page) => dispatch({ type: "SET_PAGE", payload: page }),
    setPeople: (people) => dispatch({ type: "SET_PEOPLE", payload: people }),
    fetchPeople: () => fetchPeople(dispatch),
    currentPage: state.currentPage,
    people: state.people
  };
  return (
    <MyContext.Provider value={providerValue}>{children}</MyContext.Provider>
  );
};

index.js

import React from "react";
import ReactDOM from "react-dom";

import { MyProvider } from "../src/MyProvider";

import { PeoplePageComponent } from "../src/PeoplePageComponent";

const App = () => {
  return (
    <MyProvider>
      <main>
        <PeoplePageComponent />
      </main>
    </MyProvider>
  );
};

ReactDOM.render(<App />, document.getElementById("container"));

PeoplePageComponent.test.jsx

import { render } from "@testing-library/react";
import { PagePeopleComponent } from "../PeoplePageComponent";
import { MyContext } from "../MyProvider";

const mockedContextValue = {
  setPage: jest.fn(),
  setPeople: jest.fn(),
  currentPage: "loading",
  people: null
};

test("sets page correctly", () => {
  const contextValue = {
    ...mockedContextValue
  };
  const { rerender, container } = render(
    <MyContext.Provider value={contextValue}>
      <PagePeopleComponent />
    </MyContext.Provider>
  );

  expect(container).toHaveTextContent(/This is the loading page/i);

  rerender(
    <MyContext.Provider value={{ ...contextValue, nominees: [] }}>
      <PagePeopleComponent />
    </MyContext.Provider>
  );

  expect(container).toHaveTextContent(/This is the hasPeople page/i);
});


1 Answer 1

2

Without changing the application code, you could not mock the context value, but the fetchPeople function. I assume in the real code it is a network request, thus it should be mocked anyway. Moreover not mocking the context provider makes the test more robust because it resemble more the way the software is used. In fact, it does not simulate context updates and it tests how the page manipulate the context.

import { render, screen } from "@testing-library/react";
import { PeoplePageComponent } from "../PeoplePageComponent";
import { MyProvider } from "../MyProvider";
import * as api from "../fetchPeople";

beforeEach(() => {
  jest.spyOn(api, "fetchPeople").mockImplementation(async (dispatch) => {
    setTimeout(() => {
      dispatch({
        type: "SET_PEOPLE",
        payload: [
          { name: "Tommy", age: 24 },
          { name: "John", age: 25 },
        ],
      });
    }, 1);
  });
});

test("sets page correctly", async () => {
  render(
    <MyProvider>
      <PeoplePageComponent />
    </MyProvider>
  );

  const loadingText = screen.getByText(/This is the loading page/i);
  expect(loadingText).toBeInTheDocument();
  expect(api.fetchPeople).toHaveBeenCalledTimes(1);

  const peopleText = await screen.findByText(/This is the hasPeople page/i);
  expect(peopleText).toBeInTheDocument();
});

Sign up to request clarification or add additional context in comments.

1 Comment

I got console.error Warning: An update to ... inside a test was not wrapped in act(...). When testing, code that causes React state updates should be wrapped into act(...):

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.