Reputation: 196
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:
loading
mockFetchPeople
was called 1 timemockSetCurrentPage
was called 1 timehasPeople
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
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