Skip to content
Advertisement

How to mock the useState hook implementation so that it actually changes the state during testing

I’m trying to test a component which renders two different sub-components when its internal state changes from false to true: when it’s false it renders a button that, if pressed, changes the state from false to true and renders the other one. The other is a form that on submit does the opposite.

I’ve tried to spy on the useState hook to test if it’s actually called. But by mocking the module, the actual setState won’t work when I need it in the second part of the test, to test the form that renders after.

This is my component:

import React, { useState } from 'react';

const MyComponent = ({handleChange, handleInput}) => {
     const [state, setState] = useState(false);

     return (
       <div>
         {!state
           ? (
             <button
               data-test="button1"
               type="button"
               onClick={() => setState(true)}
             >
               RenderForm
             </button>
           )
           : (
             <form onSubmit={() => setState(false)}>
               <input type="text" onChange={e => handleChange(e)} />
               <button type="submit">
                 Submit Form
               </button>
               <button type="button" onClick={() => setState(false)}>
                 Go Back
               </button>
             </form>
           )
         }
       </div>
     );
   };
export default MyComponent;

This is my test:

import React from 'react';
import { mount } from 'enzyme';
import MyComponent from './MyComponent';


describe('MyComponent', () => {
    let component;

    const mockChange = jest.fn();
    const mockSubmit = jest.fn();
    const setState = jest.fn();
    const useStateSpy = jest.spyOn(React, 'useState');
    useStateSpy.mockImplementation(init => [init, setState]);

    beforeEach(() => {
       component = mount(<MyComponent handleChange={mockChange} handleSubmit={mockSubmit}/>);
    });

    afterEach(() => {
      component.unmount();
    });

    it('calls setState when pressing btn', ()=> {
       component
         .find('[data-test="button1"]')
         .simulate('click')
       expect(setState).toHaveBeenCalledWith(true) // passes
    })
    it('calls handleChange when input changes value', () => {
       component
         .find('[data-test="button1"]') //can't be found
         .simulate('click') 
       component
         .find('input')
         .simulate('change', { target: { value: 'blabla' }}) 
       expect(mockChange).toHaveBeenCalled() // doesn't pass  
  })

  });

I know what’s the problem, but I don’t know how to fix it. Is there a way to mock setState? Or is there a way to split the tests so that they don’t interfere with each other?

Advertisement

Answer

you should probably not test the internal implementation (e.g. the state in useState etc), but only test the external functionality (click on button changes output).

This make it easier to test your code, you actually test what you want to test and not how it is implemented and if you change the implementation (rename variable for example) you will get a false negative because the code works fine (does not care for variable name and the correct component will be rendered) but your tests will fail, because you changed the name of a variable for example.

This will make it more cumbersome to fix your tests later if you change the code. If you have a large codebase, you want to know if your code works and not how it is implemented.

Hope this helps. Happy coding.

Advertisement