DEV Community

loading...
Cover image for How to test React Hooks?

How to test React Hooks?

mgm793 profile image Marc Garcia i Mullon ・3 min read

In this post I want to explain how to test most popular React Hooks using jest and enzyme.

To test react lifecycle, we need to use mount instead of using shallow.

useState

To test useState I created a small component with a title and a button to change that title.

function App() {
  const [title, setTitle] = React.useState('');
  return (
    <div className="App">
      <h1>{title}</h1>
      <button 
        data-testid="AppButton"
        onClick={() => setTitle('Another Title')}
      >
        Change Title
      </button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

To test it, we just need to simulate a button click and then see if the text is correctly updated. To simulate this click we will use an enzyme function called simulate who receive multiple parameters, but in this case we just need the first one that indicates the action to simulate ('click', 'change', 'mouseEnter'...).

test('App useState', () => {
  const wrapper = mount(<App />);
  expect(wrapper.find('[data-testid="AppTitle"]').text()).toBe('React Hooks Testing');
  wrapper.find('[data-testid="AppButton"]').simulate('click');
  expect(wrapper.find('[data-testid="AppTitle"]').text()).toBe('Another Title');
})
Enter fullscreen mode Exit fullscreen mode

useCallback

To test useCallback I created a small component with only one button. It has a function that console.log "You clicked" and the buttonName Prop and as you can see, this function is only reassigned when that buttonName changes.

function App({buttonName}) {
  const clickHandler = React.useCallback(() => {
    console.log(`You clicked ${buttonName}!`);
  }, [buttonName]);
  return (
    <div className="App">
      <button 
        data-testid="AppButton"
        onClick={clickHandler}
      >
        Click me!
      </button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

In this case we want to check if the callback function is correctly written. To do it, we just need to update the received props using an enzyme function called setProps. This function receives one parameter that is the new props that you want to change.

test('App useCallback', () => {
  const cl = console.log;
  console.log = jest.fn();
  const wrapper = mount(<App buttonName='First'/>);
  wrapper.find('[data-testid="AppButton"]').simulate('click');
  expect(console.log).toHaveBeenCalledWith('You clicked First!');
  wrapper.setProps({buttonName: 'Second'});
  wrapper.find('[data-testid="AppButton"]').simulate('click');
  expect(console.log).toHaveBeenCalledWith('You clicked Second!');
  console.log = cl;
});
Enter fullscreen mode Exit fullscreen mode

useEffect

To test useEffect, I created a small component that returns an empty div, but I wrote a useEffect more complex. In this case I set one interval who execute a console.log every second. Moreover, this useEffect has an unmount return, to clean the interval when the component is unmounted.

function App({text}) {
  useEffect(() => {
    const inter = setInterval(() => {
      console.log(text);
    }, 1000)
    return () => {
      clearInterval(inter);
      console.log('Unmount');
    }
  },[]);
  return (
    <div className="App"></div>
  );
}
Enter fullscreen mode Exit fullscreen mode

To test it, we need to mock console log and use a Fake Timers (jest) as you can see below. In this case I tested three possible cases. In the first one we mount App component and one second later we unmount it. In the second one we do the same, but in this case waiting four seconds. And in the last one we unmount App component in less than one second after mounting it.

In all this tests we check the text before and after some time, and also we are checking if the return of useEffect is called when we unmount the App component.

describe('App useState', () => {
  let cl;
  beforeEach(() => {
    cl = console.log;
    console.log = jest.fn();
    jest.useFakeTimers();
  })
  afterEach(() => {
    console.log.mockClear();
  });
  afterAll(() => {
    console.log = cl;
  });
  test('Mount and wait one second then unmount', () => {
    const wrapper = mount(<App text='Some Text'/>);
    jest.advanceTimersByTime(1000);
    expect(console.log).toHaveBeenCalledTimes(1);
    expect(console.log).toHaveBeenCalledWith('Some Text');
    console.log.mockClear();
    wrapper.unmount();
    expect(console.log).toHaveBeenCalledTimes(1);
    expect(console.log).toHaveBeenCalledWith('Unmount');
  });
  test('Mount and wait four second then unmount', () => {
    const wrapper = mount(<App text='Some Text'/>);
    jest.advanceTimersByTime(4000);
    expect(console.log).toHaveBeenCalledTimes(4);
    expect(console.log).toHaveBeenCalledWith('Some Text');
    console.log.mockClear();
    wrapper.unmount();
    expect(console.log).toHaveBeenCalledTimes(1);
    expect(console.log).toHaveBeenCalledWith('Unmount');
  });
  test('Mount and unmount in less than a second', () => {
    const wrapper = mount(<App text='Some Text'/>);
    wrapper.unmount();
    expect(console.log).toHaveBeenCalledTimes(1);
    expect(console.log).toHaveBeenCalledWith('Unmount');
  });

  console.log = cl;
});
Enter fullscreen mode Exit fullscreen mode

I hope it was interesting and helpful for you, let me know if you want other hooks or if you have some more questions.

Discussion (0)

pic
Editor guide