Search code examples
reactjsreact-hooksjestjsimmer.js

How to test the setter of a useImmer react hook with jest?


I have a parent component where a piece of state is defined with useImmer like this:

const [example, setExample] = useImmer({
submitterId: {
    value: EMPTY_STRING,
    label: 'Submitter ID',
    disabled: false,
    status: null,
    schema: submitterIdSchema
  },
  submitterName: {
    value: EMPTY_STRING,
    label: 'Submitter Name',
    disabled: false,
    status: null,
    schema: submitterNameSchema
  }});

These states are passed down as props to a child component which I am trying to write jest tests for. This is a simplified example.

export function ChildComponent({
 example,
 setExample,
}) {

  const handleInputChange = async (e, fieldName) => {
    const valueToUpdateWith = e.target.value;
    const schema = example[fieldName].yupSchema;
    const status = await validate(valueToUpdateWith, schema);
   setExample((draft) => {
      draft[fieldName].status = status;
      draft[fieldName].value = valueToUpdateWith;
    });
  };

  return (
    <div id="exampleContainer">
      <ConnectedFieldGenerator // this just loops through keys in example and generates label/input field groups for them
        fieldKeys={Object.keys(example)}
        handleInputChange={handleInputChange}
      />
    </div>
  );
}

I have written the below test in order to cover the handleInputChange function, but in my coverage report the logic inside of setExample is notcovered.

const props = {
  example: EXAMPLE_INITIAL_FIELD_STATE,
  setExample: jest.fn(),
};

    describe('Search Table Renders', () => {
      it('input change of example', async () => {
        const { container } = render(<ChildComponent{...props} />);
        await enterInputValue(container, '#submitterId-input', 'Submitter ID');
        expect(props.setBasicSearchState).toHaveBeenCalled();
      });
    });

How would I cover the below lines in handleInputChange with my tests?

setExample((draft) => {
  draft[fieldName].status = status; // NOT COVERED
  draft[fieldName].value = valueToUpdateWith; // NOT COVERED
});

Solution

  • You don't need to mock the setExample function, create a parent component for testing purposes. Keep it as simple as possible, retaining enough logic for a single test case. Then, pass the real setExample function returned by the useImmer hook to the <ChildComponent/> component. Finally, assert the latest state the component has rendered.

    E.g.

    Child.jsx:

    import React from 'react';
    
    export function ChildComponent({ example, setExample }) {
      const handleInputChange = async (e, fieldName) => {
        const valueToUpdateWith = e.target.value;
        const status = 'ok';
        setExample((draft) => {
          draft[fieldName].status = status;
          draft[fieldName].value = valueToUpdateWith;
        });
      };
    
      return (
        <div id="exampleContainer">
          <input type="text" onChange={(e) => handleInputChange(e, 'submitterId')} />
          <div>example.submitterId: {example.submitterId.value}</div>
          <div>example.submitterId.status: {example.submitterId.status}</div>
        </div>
      );
    }
    

    Child.test.jsx:

    import React from 'react';
    import { fireEvent, render, screen } from '@testing-library/react';
    import '@testing-library/jest-dom';
    import { ChildComponent } from './Child';
    import { useImmer } from 'use-immer';
    
    describe('ChildComponent', () => {
      test('should pass', () => {
        // Component for testing purposes, keep it simple.
        const ParentComp = () => {
          const [example, setExample] = useImmer({
            submitterId: {
              value: '',
              label: 'Submitter ID',
              status: null,
            },
            submitterName: {
              value: '',
              label: 'Submitter Name',
              status: null,
            },
          });
    
          return <ChildComponent example={example} setExample={setExample} />;
        };
        render(<ParentComp />);
        const textbox = screen.getByRole('textbox');
        fireEvent.change(textbox, { target: { value: 'test' } });
        expect(screen.getByText('example.submitterId: test')).toBeInTheDocument();
      });
    });
    
    

    Test result:

     PASS  stackoverflow/77582125/Child.test.tsx                                                                                                                                                                                
      ChildComponent
        √ should pass (48 ms)                                                                                                                                                                                                   
                                                                                                                                                                                                                                
    -----------|---------|----------|---------|---------|-------------------                                                                                                                                                    
    File       | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s                                                                                                                                                     
    -----------|---------|----------|---------|---------|-------------------
    All files  |     100 |      100 |     100 |     100 | 
     Child.tsx |     100 |      100 |     100 |     100 | 
    -----------|---------|----------|---------|---------|-------------------
    Test Suites: 1 passed, 1 total
    Tests:       1 passed, 1 total
    Snapshots:   0 total
    Time:        1.736 s
    Ran all test suites related to changed files.