Sandy Garrido
Sandy Garrido

Reputation: 196

testing-library react: test changes that is triggered(useEffect) by context functions and variables

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:

I want to test:

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);
});


Upvotes: 2

Views: 4472

Answers (1)

Mirco Bellagamba
Mirco Bellagamba

Reputation: 1218

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();
});

Upvotes: 2

Related Questions