Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Testing useContext() with react-testing-library

I think I found another way to test a component using the useContext hook. I have seen a few tutorials that test if a value can be successfully passed down to a child component from a parent Context Provider but did not find tutorials on the child updating the context value.

My solution is to render the root parent component along with the provider, because the state is ultimately changed in the root parent component and then passed to the provider which then passes it to all child components. Right?

The tests seem to pass when they should and not pass when they shouldn't. Can someone explain why this is or isn't a good way to test the useContext hook?

The root parent component:

...
const App = () => {
  const [state, setState] = useState("Some Text")

  const changeText = () => {
    setState("Some Other Text")
  }
...

  <h1> Basic Hook useContext</h1>
     <Context.Provider value={{changeTextProp: changeText,
                               stateProp: state
                                 }} >
        <TestHookContext />
     </Context.Provider>
)}

The context object:

import React from 'react';

const Context = React.createContext()

export default Context

The child component:

import React, { useContext } from 'react';

import Context from '../store/context';

const TestHookContext = () => {
  const context = useContext(Context)

  return (
    <div>
    <button onClick={context.changeTextProp}>
        Change Text
    </button>
      <p>{context.stateProp}</p>
    </div>
  )
}

And the tests:

import React from 'react';
import ReactDOM from 'react-dom';
import TestHookContext from '../test_hook_context.js';
import {render, fireEvent, cleanup} from '@testing-library/react';
import App from '../../../App'

import Context from '../../store/context';

afterEach(cleanup)

it('Context is updated by child component', () => {

   const { container, getByText } = render(<App>
                                            <Context.Provider>
                                             <TestHookContext />
                                            </Context.Provider>
                                           </App>);

   console.log(container)
   expect(getByText(/Some/i).textContent).toBe("Some Text")

   fireEvent.click(getByText("Change Text"))

   expect(getByText(/Some/i).textContent).toBe("Some Other Text")
})
like image 633
iqbal125 Avatar asked Dec 18 '25 22:12

iqbal125


1 Answers

The problem with the approach that you mention is coupling. Your Context to be tested, depends on <TestHookContext/> and <App/>

As Kent C. Dodds, the author of react-testing-library, has a full article on "Test Isolation with React" if you want to give it a read.

TLDR: Demo repo here

How to test Context

  • Export a <ContextProvider> component that holds the state and returns <MyContext.Provider value={{yourWhole: "State"}}>{children}<MyContext.Provider/> This is the component that we are going to test for the provider.
  • On the components that consume that Context, create a MockContextProvider to replace the original one. You want to test the component in isolation.
  • You can test the whole app workflow to by testing the root component.

Testing Auth Provider

Let's say we have a component that provides Auth using context the following way:

import React, { createContext, useState } from "react";

export const AuthContext = createContext();

const AuthProvider = ({ children }) => {
  const [isLoggedin, setIsLoggedin] = useState(false);
  const [user, setUser] = useState(null);

  const login = (user) => {
    setIsLoggedin(true);
    setUser(user);
  };

  const logout = () => {
    setIsLoggedin(false);
    setUser(null);
  };

  return (
    <AuthContext.Provider value={{ logout, login, isLoggedin, user }}>
      {children}
    </AuthContext.Provider>
  );
};

export default AuthProvider;

The test file would look like:

import { fireEvent, render, screen } from "@testing-library/react";
import AuthProvider, { AuthContext } from "./AuthProvider";
import { useContext } from "react";

const CustomTest = () => {
  const { logout, login, isLoggedin, user } = useContext(AuthContext);

  return (
    <div>
      <div data-testid="isLoggedin">{JSON.stringify(isLoggedin)}</div>
      <div data-testid="user">{JSON.stringify(user)}</div>
      <button onClick={() => login("demo")} aria-label="login">
        Login
      </button>
      <button onClick={logout} aria-label="logout">
        LogOut
      </button>
    </div>
  );
};

test("Should render initial values", () => {
  render(
    <AuthProvider>
      <CustomTest />
    </AuthProvider>
  );

  expect(screen.getByTestId("isLoggedin")).toHaveTextContent("false");
  expect(screen.getByTestId("user")).toHaveTextContent("null");
});

test("Should Login", () => {
  render(
    <AuthProvider>
      <CustomTest />
    </AuthProvider>
  );
  const loginButton = screen.getByRole("button", { name: "login" });
  fireEvent.click(loginButton);
  expect(screen.getByTestId("isLoggedin")).toHaveTextContent("true");
  expect(screen.getByTestId("user")).toHaveTextContent("demo");
});

test("Should Logout", () => {
  render(
    <AuthProvider>
      <CustomTest />
    </AuthProvider>
  );
  const loginButton = screen.getByRole("button", { name: "logout" });
  fireEvent.click(loginButton);
  expect(screen.getByTestId("isLoggedin")).toHaveTextContent("false");
  expect(screen.getByTestId("user")).toHaveTextContent("null");
});

Testing Component that consumes Context

import React, { useContext } from "react";
import { AuthContext } from "../context/AuthProvider";

const Welcome = () => {
  const { logout, login, isLoggedin, user } = useContext(AuthContext);

  return (
    <div>
      {user && <div>Hello {user}</div>}
      {!user && <div>Hello Anonymous Goose</div>}
      {!isLoggedin && (
        <button aria-label="login" onClick={() => login("Jony")}>
          Log In
        </button>
      )}
      {isLoggedin && (
        <button aria-label="logout" onClick={() => logout()}>
          Log out
        </button>
      )}
    </div>
  );
};

export default Welcome;

We will mock the AuthContext value by providing one of our own:

import React, { useContext } from "react";
import { render, screen } from "@testing-library/react";
import "@testing-library/jest-dom";
import Welcome from "./welcome";
import userEvent from "@testing-library/user-event";
import { AuthContext } from "../context/AuthProvider";

// A custom provider, not the AuthProvider, to test it in isolation.
// This customRender will be a fake AuthProvider, one that I can controll to abstract of AuthProvider issues.

const customRender = (ui, { providerProps, ...renderOptions }) => {
  return render(
    <AuthContext.Provider value={providerProps}>{ui}</AuthContext.Provider>,
    renderOptions
  );
};

describe("Testing Context Consumer", () => {
  let providerProps;
  beforeEach(
    () =>
      (providerProps = {
        user: "C3PO",
        login: jest.fn(function (user) {
          providerProps.user = user;
          providerProps.isLoggedin = true;
        }),
        logout: jest.fn(function () {
          providerProps.user = null;
          providerProps.isLoggedin = false;
        }),
        isLoggedin: true,
      })
  );

  test("Should render the user Name when user is signed in", () => {
    customRender(<Welcome />, { providerProps });
    expect(screen.getByText(/Hello/i)).toHaveTextContent("Hello C3PO");
  });

  test("Should render Hello Anonymous Goose when is NOT signed in", () => {
    providerProps.isLoggedin = false;
    providerProps.user = null;
    customRender(<Welcome />, { providerProps });
    expect(screen.getByText(/Hello/i)).toHaveTextContent(
      "Hello Anonymous Goose"
    );
  });

  test("Should render Logout button when user is signed in", () => {
    customRender(<Welcome />, { providerProps });
    expect(screen.getByRole("button", { name: "logout" })).toBeInTheDocument();
    expect(screen.queryByRole("button", { name: "login" })).toBeNull();
  });

  test("Should render Login button when user is NOT signed in", () => {
    providerProps.isLoggedin = false;
    providerProps.user = null;
    customRender(<Welcome />, { providerProps });
    expect(screen.getByRole("button", { name: "login" })).toBeInTheDocument();
    expect(screen.queryByRole("button", { name: "logout" })).toBeNull();
  });

  test("Should Logout when user is signed in", () => {
    const { rerender } = customRender(<Welcome />, { providerProps });
    const logout = screen.getByRole("button", { name: "logout" });
    expect(logout).toBeInTheDocument();
    expect(screen.queryByRole("button", { name: "login" })).toBeNull();
    userEvent.click(logout);
    expect(providerProps.logout).toHaveBeenCalledTimes(1);

    //Technically, re renders are responsability of the parent component, but since we are here...
    rerender(
      <AuthContext.Provider value={providerProps}>
        <Welcome />
      </AuthContext.Provider>
    );
    expect(screen.getByText(/Hello/i)).toHaveTextContent(
      "Hello Anonymous Goose"
    );
    expect(screen.getByRole("button", { name: "login" })).toBeInTheDocument();
    expect(screen.queryByRole("button", { name: "logout" })).toBeNull();
  });

  test("Should Login when user is NOT signed in", () => {
    providerProps.isLoggedin = false;
    providerProps.user = null;
    const { rerender } = customRender(<Welcome />, { providerProps });
    const login = screen.getByRole("button", { name: "login" });
    expect(login).toBeInTheDocument();
    expect(screen.queryByRole("button", { name: "logout" })).toBeNull();
    userEvent.click(login);
    expect(providerProps.login).toHaveBeenCalledTimes(1);

    //Technically, re renders are responsability of the parent component, but since we are here...
    rerender(
      <AuthContext.Provider value={providerProps}>
        <Welcome />
      </AuthContext.Provider>
    );
    expect(screen.getByText(/Hello/i)).toHaveTextContent("Hello Jony");
    expect(screen.getByRole("button", { name: "logout" })).toBeInTheDocument();
    expect(screen.queryByRole("button", { name: "login" })).toBeNull();
  });
});
like image 65
Jonatan Kruszewski Avatar answered Dec 20 '25 18:12

Jonatan Kruszewski



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!