DEV Community

Andrew Givens
Andrew Givens

Posted on • Edited on

53 5

Testing React Hook State Changes

Edit (2020): I would recommend switching over to react-testing-library, and changing your testing approach to test how your component changes with state rather than that state changed. While this approach works, it is not the ideal solution to the problem. I originally came up with this solution because I started React work using class components and being able to use the enzyme helpers to directly test my values of state against what I expected. With hooks, the testing approach has changed and that is no longer ideal. We should be testing our full component and how the component responds to the state change, not that state has changed.

With the introduction of React Hooks, testing our components state changes is not as straight forward as it used to be. However, it is still possible to test these state changes directly. It just requires a little mocking. 🤠

Testing state change with a class component

Previously, if you used a React Class Component, you could simply read and manipulate the component state from the shallow object enzyme provides us through shallow rendering.

import React from 'react';
class TestComponent extends React.PureComponent {
constructor(props) {
super(props);
this.state = { count: 0 };
}
render() {
return (
<h3>{this.state.count}</h3>
<span>
<button id="count-up" type="button" onClick={() => this.setState({ count: this.state.count + 1 })}>
Count Up!
</button>
<button id="count-down" type="button" onClick={() => this.setState({ count: this.state.count - 1 })}>
Count Down!
</button>
<button id="zero-count" type="button" onClick={() => this.setState({ count: 0 })}>Zero</button>
</span>
)
}
}
export default TestComponent;
import React from 'react';
import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import TestComponent from './ClassComponent';
Enzyme.configure({ adapter: new Adapter() });
describe('<TestComponent />', () => {
let wrapper;
beforeEach(() => {
wrapper = Enzyme.shallow(<TestComponent />);
});
it('has the initial state count of zero', () => {
expect(wrapper.state()).toEqual({ count: 0 });
})
describe('The Count Up Button', () => {
it('increments state count by 1 on click', () => {
wrapper.find('#count-up').props().onClick();
expect(wrapper.state()).toEqual({ count: 1 });
});
});
describe('The Count Down Button', () => {
it('decrements state count by 1 on click', () => {
wrapper.find('#count-down').props().onClick();
expect(wrapper.state()).toEqual({ count: -1 });
});
});
describe('The Count Zero Button', () => {
it('sets state count to 0 on click', () => {
wrapper.setState({ count: 10 });
wrapper.find('#zero-count').props().onClick();
expect(wrapper.state()).toEqual({ count: 0 });
});
});
});

Testing state change with hooks

However, with the introduction of hooks, you can now give state to functional components through React.useState. This means that our enzyme shallow render object will not have a state() method.

Implementations I've found around this subject before talked about testing the repercussions of changing state. For example, state updates and we test that the displayed count value is what we expect, or we test that a function is called with the correct parameter from state, etc.

Which, I would say is a completely valid way to test your state change. However, it feels like it goes against the isolation we should consider when unit testing.

If I'm testing my onClick event, all I really care about for my test is that is calls setCount with whatever variable it should. We trust that React works correctly; so, my test shouldn't rely on useState updating my state variable and re-rendering my component for my unit test.

So, why don't we mock it?

import React from 'react';
const TestComponent = () => {
const [count, setCount] = React.useState(0);
return (
<h3>{count}</h3>
<span>
<button id="count-up" type="button" onClick={() => setCount(count + 1)}>Count Up</button>
<button id="count-down" type="button" onClick={() => setCount(count - 1)}>Count Down</button>
<button id="zero-count" type="button" onClick={() => setCount(0)}>Zero</button>
</span>
);
}
export default TestComponent;
import React from 'react';
import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import TestComponent from './FunctionalComponent';
Enzyme.configure({ adapter: new Adapter() });
describe('<TestComponent />', () => {
let wrapper;
const setState = jest.fn();
const useStateSpy = jest.spyOn(React, 'useState')
useStateSpy.mockImplementation((init) => [init, setState]);
beforeEach(() => {
wrapper = Enzyme.shallow(<TestComponent />);
});
afterEach(() => {
jest.clearAllMocks();
});
describe('Count Up', () => {
it('calls setCount with count + 1', () => {
wrapper.find('#count-up').props().onClick();
expect(setState).toHaveBeenCalledWith(1);
});
});
describe('Count Down', () => {
it('calls setCount with count - 1', () => {
wrapper.find('#count-down').props().onClick();
expect(setState).toHaveBeenCalledWith(-1);
});
});
describe('Zero', () => {
it('calls setCount with 0', () => {
wrapper.find('#zero-count').props().onClick();
expect(setState).toHaveBeenCalledWith(0);
});
});
});

Conclusion

With this implementation, we are mocking React.useState to return an Array with the initial value passed to the method and a jest mock function. This will set the states setter to our mock function and allow us to test that it was called with the expected value to set state to.

Pretty neat! Now there is no need for us to dig through props to check that our state gets set correctly. 👌

Top comments (23)

Collapse
 
casconed profile image
Jason Moore

This is super helpful, thank you. Can you shed any light on why importing useState:

import React, { useState } from 'react'

and calling it on line 4 of FunctionalComponent.jsx as:

const [count, setCount] = useState(0)

breaks the test? Seems like the spy should still see that getting called?

Collapse
 
theactualgivens profile image
Andrew Givens

Yup, so the issue is because we are spying on React.useState, which sets a spy on the default export from 'react'. Since you are pulling in useState as a named export, there is no spy on it.

I've messed around with mocking the named export, but it's not super straight forward. I've always done it using import * as Something from 'module';, but I can't seem to get that to work with react.

And if you mock the whole react module, you get some errors from enzyme.

Here are some of the sources I used when looking into this:
jest-mock-default-named-export
mock-spy-exported-functions-within...

If you can figure it out, please let me know because I'm very interested :)
But it seems that just calling useState from React is the easiest way. To be honest, I don't see the point in importing useState as a named export when it's already included in the default export.

Collapse
 
casconed profile image
Jason Moore

Great, thanks. I'll play around with it - we're using useState as a named export all over the place - wouldn't be a HUGE deal to convert but it'd be nice to figure out how to test as-is.

Thread Thread
 
squidsoup profile image
Kit Randel

Did you happen to find a solution?

Thread Thread
 
ppciesiolkiewicz profile image
Piotr

to make it work with useState we would have to mock it using jest while keeping rest of react intact. jest.requireActual can be used to achieve that

import React, { useState as useStateMock } from 'react';

jest.mock('react', () => ({
  ...jest.requireActual('react'),
  useState: jest.fn(),
}));

describe('Test', () => {
  const setState = jest.fn();

  beforeEach(() => {
    useStateMock.mockImplementation(init => [init, setState]);
  });

...
Enter fullscreen mode Exit fullscreen mode
Thread Thread
 
jbolotin profile image
Jonah Bolotin

The following technique works well for me testing functional components with useState destructured. This is an adapted solution from that above because the mockImplementation above caused react-test-renderer tests to fail:

import * as React from 'react';

describe('Some message', () => {
    const setState = jest.fn();
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const useStateMock: any = (initState: any) => [initState, setState];

    afterEach(() => {
      jest.clearAllMocks();
    });

    it('Is a test where we want to mock useState', () => {
          jest.spyOn(React, 'useState').mockImplementation(useStateMock);
          const wrapper = shallow(<Component {...props} />);
          // trigger setState somehow
          expect(setState).toHaveBeenCalledTimes(1);
          // Other tests here
    });
});
Enter fullscreen mode Exit fullscreen mode

This is typescript, but it should work just as well in JS if you remove the type annotations

Thread Thread
 
fredreis profile image
Fred-Reis

works fine for me

Thread Thread
 
brianle10680751 profile image
Brian Le

Thanks @Jonah, It works

Thread Thread
 
dhanvinarc profile image
Dhanvin Patel • Edited

How can we test this type of case with jest and enzyme?
const [open, setOpen] = useState(false);
const handleClose = () => {
setOpen(!open);

}
Enter fullscreen mode Exit fullscreen mode

this handleClose is an onClick event

Collapse
 
dotorimook profile image
dotorimook

How about testing for a component with multiple useStates?
Could you tell me how to detect each setState to be called?

Collapse
 
istvandesign profile image
Istvan Fulop • Edited

Hello,
Could you explain more exactly what does these do :

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

  1. How to set state with the mock implementation ?
  2. How to use with TypeScript ? What type should init have ?
  3. How to set multiple hooks set by useState ?
  4. How to mock a useState called by a promise ?
Collapse
 
kevinding0218 profile image
Kevin Ding • Edited

Hi, thanks for sharing the tutorial! However, things might still not work for me, and I am not sure how did useStateSpy connected with the Enzyme.shallow(<TestComponent />); in your test case...

In addition, my wrapper by shallow turns out to be null, but when I use mount it can show me correct component.

Collapse
 
tudormaerean profile image
tudormaerean

How do you make this work when you set the state inside a useEffect?

For me it doesn't see the mocked setState function...

Collapse
 
victor95pc profile image
Victor Palomo de Castro

If you guys wants a way that gives you full access to your react hooks's states and dispatchers without the pain to extract or modify your code.

You can use my library: github.com/victor95pc/jest-react-h...

Collapse
 
timurcatakli profile image
Timur Catakli

I am getting following error after npm install.

SyntaxError: Unexpected identifier

      1 | import React from 'react';
    > 2 | import setupMockStates from 'jest-react-hooks-mock';
        | ^
      3 | import { shallow } from 'enzyme';
Collapse
 
juuuuuuuuuuuuuu profile image
juuuuuuuuuuuuuu

how to get init state not using library ?

Collapse
 
igokul777 profile image
Igor Ku • Edited

IMHO your approach is legit but describes a creation of brittle tests - you are trying to test implementation and not the behaviour: if you decide to eventually change implementation (use class component and the real setState method on it), your test will fail and you will have to adjust it accordingly, which should be avoided. On the other hand, I do not propose a solution, because Enzyme does not propose it - they still have to catch up with hooks. One may consider switching to React Testing Library if the hooks in the app are used extensively (I recently realised that there is no need for class components any longer given the hooks have been introduced a year ago) - it has much better support for hooks and is a React-recommended testing library. If, on the other hand, it is an application that extensively uses class components, then, I guess, one should not switch to hooks for the sake of having Enzyme continue a good job of testing the state values and not their implementation.
P.S. Actually testing a state in general is a bad idea - it is better to test what is being rendered, thereby completely focusing on the result and not implementation details, as well as making my above comments valid but obsolete and the problem non-existent :-)

Collapse
 
juuuuuuuuuuuuuu profile image
juuuuuuuuuuuuuu

Testing state change with hooks,
How to test init state ??

describe('init value, () => {
it('init value', () => {
expect(setState).toHaveBeenCalledWith(0);
});
});

This code doesn't pass.
please help me how to check init state!!

Collapse
 
theactualgivens profile image
Andrew Givens

My recommendation for testing the initial state would just be test that React.useState is called with your initial state value.

Collapse
 
johhansantana profile image
Johhan Santana

Is there a way to read the state value after you call the setState mock?

Collapse
 
jigshgithub profile image
jigshGitHub

How to get actual value being set with setState in test case...in this case value of 'count' ...let me put this way...when 'Count Up' test case being run, it updates state of count by 1, if I update wrapper, should I get

1

...and how can we test