Home > Software engineering >  Mocked useState not triggering a rerender React Testing Library
Mocked useState not triggering a rerender React Testing Library

Time:07-21

Code Example

Hi, I have an unusual test case, it's not working the way I expect. I am testing a component that resides inside of a context in our app. One of the values being passed down by the context is a piece of state as well as the setter of that piece of state.

e.g

// testContext.js
import { createContext, useState } from "react";

export const TestContext = createContext(null);

export default function TestContextProvider({ children }) {
  const [stateVariable, setStateVariable] = useState(1);
  return (
    <TestContext.Provider value={{ stateVariable, setStateVariable }}>
      {children}
    </TestContext.Provider>
  );
}

-

// App.js
import * as React from "react";
import "./styles.css";
import ComponentToBeTested from "./ComponentToBeTested";
import TestContextProvider from "./testContext";

export default function App() {
  return (
    <div className="App">
      <TestContextProvider>
        <ComponentToBeTested />
      </TestContextProvider>
    </div>
  );
}

-

//ComponentToBeTested.js
import { useContext } from "react";
import { TestContext } from "./testContext";

export default function ComponentToBeTested() {
  const { stateVariable, setStateVariable } = useContext(TestContext);
  return (
    <>
      <h1>
        Current value: <span data-testid="answer">{stateVariable}</span>
      </h1>
      <button
        data-testid="increment-button"
        onClick={() => setStateVariable(stateVariable   1)}
      >
        Increment
      </button>
    </>
  );
}

Testing ComponentToBeTested.js

I am trying to test that when the use clicks the button, the value increases. I thought I could use the renderHook function to mock the useState. I also put a spy on the return value of useContext and returned my mocked implementation of the state and setter. I then simulate a button click and assert that the value shown in the UI is the same as the value returned from renderHook

e.g

  it("mocks the useState and renders state updates", () => {
    const { result } = renderHook(() => useState(1));
    jest.spyOn(React, "useContext").mockReturnValue({
      stateVariable: result.current[0],
      setStateVariable: result.current[1]
    });
    render(<ComponentToBeTested />);
    const button = screen.getByTestId("increment-button");
    fireEvent.click(button);
    const answer = parseInt(screen.getByTestId("answer").innerHTML);
    expect(answer).toEqual(result.current[0]);
  });

This is not working however. The UI does not update. I am wondering if anyone knows:

  1. Why is the component not re-rendering when the value being returned by the useContext has been updated?

  2. Is there a way to get the component to rerender manually given the updated state, I would expect it to render as expected?

  3. Is there just a better way to achieve this. I feel like I might be going all around the world to do something that is probably simple

Link to a codesandbox where you can play around with this example here

CodePudding user response:

Why is the component not re-rendering when the value being returned by the useContext has been updated?

You mock React.useContext, its mock implementation breaks the real useContext feature. This means React doesn't know how to detect the context changes because you replace useContext with your mock.

Is there just a better way to achieve this. I feel like I might be going all around the world to do something that is probably simple

Don't mock anything provided by React, incorrect mock will break the feature of React. If your test case is built on these incorrect mocks, your test cases may pass but your code may be wrong in runtime. For more info, see What you should avoid with Testing Library

Render the context provider and component as usual, use RTL queries to find the target element, and fire an event on it. Then check what has been rendered of the component.

E.g.

TestContext.tsx:

import React, { createContext, Dispatch, SetStateAction, useState } from 'react';

export const TestContext = createContext<{
  stateVariable: number;
  setStateVariable: Dispatch<SetStateAction<number>>;
}>(null!);

export default function TestContextProvider({ children }) {
  const [stateVariable, setStateVariable] = useState(1);
  return <TestContext.Provider value={{ stateVariable, setStateVariable }}>{children}</TestContext.Provider>;
}

Component.tsx:

import React from 'react';
import { useContext } from 'react';
import { TestContext } from './TestContext';

export default function ComponentToBeTested() {
  const { stateVariable, setStateVariable } = useContext(TestContext)
  return (
    <>
      <h1>
        Current value: <span data-testid="answer">{stateVariable}</span>
      </h1>
      <button data-testid="increment-button" onClick={() => setStateVariable(stateVariable   1)}>
        Increment
      </button>
    </>
  );
}

Component.test.tsx:

import React from 'react';
import { fireEvent, render, screen } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import ComponentToBeTested from './Component';
import TestContextProvider from './TestContext';

describe('73039704', () => {
  test('should pass', () => {
    render(
      <TestContextProvider>
        <ComponentToBeTested />
      </TestContextProvider>
    );
    expect(screen.getByTestId('answer')).toHaveTextContent('1');
    const button = screen.getByTestId('increment-button');
    fireEvent.click(button);
    expect(screen.getByTestId('answer')).toHaveTextContent('2');
  });
});

Test result:

 PASS  stackoverflow/73039704/Component.test.tsx (11.423 s)
  73039704
    ✓ should pass (33 ms)

-----------------|---------|----------|---------|---------|-------------------
File             | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
-----------------|---------|----------|---------|---------|-------------------
All files        |     100 |      100 |     100 |     100 |                   
 Component.tsx   |     100 |      100 |     100 |     100 |                   
 TestContext.tsx |     100 |      100 |     100 |     100 |                   
-----------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        12.116 s
  • Related