Home > front end >  Jest/React: how to check that state is set to a new value when setter receives a callback that retur
Jest/React: how to check that state is set to a new value when setter receives a callback that retur

Time:08-20

When we have

function ParentComponent() {
   const [state, setState] = useState({
     name: 'Rob',
     age: '55'
   });

   return <ChildComponent state={state} setState={setState} />;
}

and inside ChildComponent something like this happens:

setState({
  name: 'Alice',
  age: 44
});

we can do

const mockSetter = jest.fn();

render(<ChildComponent state={{ name: 'Rob', age: 55 }} setState={mockSetter} />);

// interact with ChildComponent to make it call `setState`

expect(mockSetter).toHaveBeenCalledWith({
  name: 'Alice',
  age: 55
});

But how do I test that the new state has been set to { name: 'Alice', age: 55 } in case the setState prop inside ChildComponent is called with a function instead of an object? For example:

setState(prevState => ({ ...prevState, name: 'Alice' })

In this case mockSetter will be called with a function so my original toHaveBeenCalledWith check is no longer valid. It expects an object { name: 'Alice', age: 55 } but gets a function prevState => ({ ...prevState, name: 'Alice' }.

How do I check that the state has been set to { name: 'Alice', age: 55 } after a ChildComponent interaction (even though I don't have the state variable to refer to in my tests)?

CodePudding user response:

First of all, mock setter function of the useState hook will break the real implementation of it. Your test may pass based on the mock setter function, but the actual code may not run correctly.

But if you insist to do so, you can use jest.fn().mockImplementation() to create mock implementation for the setter function of useState. (Not recommended!! React doesn't know how to update the state anymore with this mock setter function!)

index.tsx:

import React, { useState } from 'react';

export const ChildComponent = ({ state, setState }) => {
  return (
    <div
      onClick={() => {
        setState((prevState) => ({ ...prevState, name: 'Alice' }));
      }}
    >
      ChildComponent
    </div>
  );
};

index.test.tsx:

import { fireEvent, render, screen } from '@testing-library/react';
import React from 'react';
import { ChildComponent } from './';

describe('73406702', () => {
  test('should pass', () => {
    const prevState = { name: 'Rob', age: 55 };
    let nextState;
    const mockSetter = jest.fn().mockImplementation((callback) => {
      nextState = callback(prevState);
    });
    render(<ChildComponent state={prevState} setState={mockSetter} />);
    fireEvent.click(screen.getByText(/ChildComponent/));
    expect(nextState).toEqual({ name: 'Alice', age: 55 });
  });
});

We can provide prevState by ourselves, and get the nextState.

This mock implementation is only used for testing the state merging logic { ...prevState, name: 'Alice' }. React doesn't know how to update the state and re-render the component anymore with this mock setter function. Because we didn't provide this feature for the mock setter function. The real implementation of the setter function is complicated.

Test result:

 PASS  stackoverflow/73406702/index.test.tsx (10.451 s)
  73406702
    ✓ should pass (28 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        10.948 s, estimated 12 s

In the end, the testing philosophy for react component, or, any UI component is to test the component behavior stand from the user's perspective. You can think it's black-box testing.

  • Related