DEV Community

Dinesh Pandiyan
Dinesh Pandiyan

Posted on

React Hooks: Test custom hooks with Enzyme

TL;DR - Wrap your custom hook in a component and shallow render it to test implementation details.

What you will learn

  • React test strategies
    • user observable behaviour
    • implementation details
  • Testing custom hooks with Enzyme

Test Strategies

There are broadly two strategies to test our React codebase.

  1. Testing user observable behaviour
  2. Testing implementation details

Testing user observable behaviour

Testing user observable behaviour means writing tests against components that test

  • how the component is rendered
  • how the component is re-rendered when user interacts with the DOM
  • how props/state control what is rendered

Consider the following component - Greet

function Greet({ user = 'User' }) {
  const [name, setName] = React.useState(user);

  return <div onClick={() => setName('Pinocchio')}>Hello, {name}!</div>;
}
Enter fullscreen mode Exit fullscreen mode

Testing the user observable behaviour in Greet would mean

  • test if Greet is rendered without crashing
  • test if Hello, User! is rendered when user prop is not passed
  • test if Hello, Bruce! is rendered when Bruce is passed as value to user prop
  • test if the text changes to Hello, Pinocchio! when the user clicks on the element

Testing implementation details

Testing implementation details means writing tests against state logic that test

  • how the state is initialized with default/prop values
  • how the state changes when handlers are invoked

Consider the same component - Greet

function Greet({ user = 'User' }) {
  const [name, setName] = React.useState(user);

  return <div onClick={() => setName('Pinocchio')}>Hello, {name}!</div>;
}
Enter fullscreen mode Exit fullscreen mode

Testing implementation details in Greet would mean

  • test if name is set to default value User when user prop is not passed to Greet
  • test if name is set to prop value when user prop is passed to Greet
  • test if name is updated when setName is invoked

Testing custom hooks with Enzyme

Note: Please make sure your React version is ^16.8.5. Hooks won't re-render components with enzyme shallow render in previous versions and the React team fixed it in this release. If your React version is below that, you might have to use enzyme mount and .update() your wrapper after each change to test the re-render.

Testing implementation details might seem unnecessary and might even be considered as a bad practice when you are writing tests against components that contains presentational (UI) logic and render elements to the DOM. But custom hooks contain only state logic and it is imperative that we test the implementation details thoroughly so we know exactly how our custom hook will behave within a component.

Let's write a custom hook to update and validate a form field.

/* useFormField.js */

import React from 'react';

function useFormField(initialVal = '') {
  const [val, setVal] = React.useState(initialVal);
  const [isValid, setValid] = React.useState(true);

  function onChange(e) {
    setVal(e.target.value);

    if (!e.target.value) {
      setValid(false);
    } else if (!isValid) setValid(true);
  }

  return [val, onChange, isValid];
}

export default useFormField;
Enter fullscreen mode Exit fullscreen mode

As great as custom hooks are in abstracting away re-usable logic in our code, they do have one limitation. Even though they are just JavaScript functions they will work only inside React components. You cannot just invoke them and write tests against what a hook returns. You have to wrap them inside a React component and test the values that it returns.

  • custom hooks cannot be tested like JavaScript functions
  • custom hooks should be wrapped inside a React component to test its behaviour

Thanks to the composibility of hooks, we could pass a hook as a prop to a component and everything will work exactly as how it's supposed to work. We can write a wrapper component to render and test our hook.

/* useFormField.test.js */

function HookWrapper(props) {
  const hook = props.hook ? props.hook() : undefined;
  return <div hook={hook} />;
}
Enter fullscreen mode Exit fullscreen mode

Now we can access the hook like a JavaScript object and test its behaviour.

/* useFormField.test.js */

import React from 'react';
import { shallow } from 'enzyme';
import useFormField from './useFormField';

function HookWrapper(props) {
  const hook = props.hook ? props.hook() : undefined;
  return <div hook={hook} />;
}

it('should set init value', () => {
  let wrapper = shallow(<HookWrapper hook={() => useFormField('')} />);

  let { hook } = wrapper.find('div').props();
  let [val, onChange, isValid] = hook;
  expect(val).toEqual('');

  wrapper = shallow(<HookWrapper hook={() => useFormField('marco')} />);

  // destructuring objects - {} should be inside brackets - () to avoid syntax error
  ({ hook } = wrapper.find('div').props());
  [val, onChange, isValid] = hook;
  expect(val).toEqual('marco');
});
Enter fullscreen mode Exit fullscreen mode

The full test suite for useFormField custom hook will look like this.

/* useFormField.test.js */

import React from 'react';
import { shallow } from 'enzyme';
import useFormField from './useFormField';

function HookWrapper(props) {
  const hook = props.hook ? props.hook() : undefined;
  return <div hook={hook} />;
}

describe('useFormField', () => {
  it('should render', () => {
    let wrapper = shallow(<HookWrapper />);

    expect(wrapper.exists()).toBeTruthy();
  });

  it('should set init value', () => {
    let wrapper = shallow(<HookWrapper hook={() => useFormField('')} />);

    let { hook } = wrapper.find('div').props();
    let [val, onChange, isValid] = hook;
    expect(val).toEqual('');

    wrapper = shallow(<HookWrapper hook={() => useFormField('marco')} />);

    // destructuring objects - {} should be inside brackets - () to avoid syntax error
    ({ hook } = wrapper.find('div').props());
    [val, onChange, isValid] = hook;
    expect(val).toEqual('marco');
  });

  it('should set the right val value', () => {
    let wrapper = shallow(<HookWrapper hook={() => useFormField('marco')} />);

    let { hook } = wrapper.find('div').props();
    let [val, onChange, isValid] = hook;
    expect(val).toEqual('marco');

    onChange({ target: { value: 'polo' } });

    ({ hook } = wrapper.find('div').props());
    [val, onChange, isValid] = hook;
    expect(val).toEqual('polo');
  });

  it('should set the right isValid value', () => {
    let wrapper = shallow(<HookWrapper hook={() => useFormField('marco')} />);

    let { hook } = wrapper.find('div').props();
    let [val, onChange, isValid] = hook;
    expect(val).toEqual('marco');
    expect(isValid).toEqual(true);

    onChange({ target: { value: 'polo' } });

    ({ hook } = wrapper.find('div').props());
    [val, onChange, isValid] = hook;
    expect(val).toEqual('polo');
    expect(isValid).toEqual(true);

    onChange({ target: { value: '' } });

    ({ hook } = wrapper.find('div').props());
    [val, onChange, isValid] = hook;
    expect(val).toEqual('');
    expect(isValid).toEqual(false);
  });
});
Enter fullscreen mode Exit fullscreen mode

Rendering the custom hook and accessing it as a prop should give us full access to its return values.

If you're using useEffect hook in your custom hook, make sure you wrap the shallow or mount call with ReactTestUtils.act() to have the effects flushed out before assertions. Enzyme might support this internally soon but for now, this is required. More info on this here - hooks-faq.

act(() => {
  wrapper = shallow(<HookWrapper />);
});
Enter fullscreen mode Exit fullscreen mode

All code snippets in this post can be found in the repo - testing-hooks with a working example.

Happy testing! 🎉

Top comments (12)

Collapse
 
robertcoopercode profile image
Robert Cooper

Thanks for the post 👍🏼

I had a heck of a time getting enzyme to work with functional react components that use hooks this week. I've instead ended up adopting react-testing-library and no longer test for implementation details (you can't really test for implementation details with react-testing-library as its API doesn't allow for it). I also read Kent C Dodds post regarding his thoughts on testing implementation details, and it convinced me to use his library.

Collapse
 
pgangwani profile image
pgangwani

Good Work Dinesh !! Really helpful.

I have been working on hooks testing in enzyme and opened a PR some time back here --}github.com/airbnb/enzyme/pull/2041

Would you like to contribute or help?

Collapse
 
flexdinesh profile image
Dinesh Pandiyan

Thanks Pawan. I'll try to take a look when I find time.

Collapse
 
pretaporter profile image
Maksim

Try github.com/mpeyper/react-hooks-tes.... I think it's little bit easier.

Collapse
 
mpeyper profile image
Michael Peyper

Thank you so much!

Collapse
 
victor95pc profile image
Victor Palomo de Castro

Another one is my library github.com/victor95pc/jest-react-h...

Collapse
 
scriptkavi profile image
ScriptKavi

Many early birds have already started using this custom hooks library
in their ReactJs/NextJs project.

Have you started using it?

scriptkavi/hooks

PS: Don't be a late bloomer :P

Collapse
 
stereobooster profile image
stereobooster

use useMemo or useCallback for onChange function inside your hook

Collapse
 
flexdinesh profile image
Dinesh Pandiyan

I understand why we would need to use useMemo or useCallback but to keep the example simple, it's a good idea to avoid them.

Collapse
 
ammoradi profile image
Amir Mohammad

Amazing! Thank you <3

Collapse
 
josuevalrob profile image
Josue Valenica

what about use effect??

Collapse
 
victor95pc profile image
Victor Palomo de Castro

If you guys wants to test react hooks having access to your internal states and dispatchers use my library, is super simple: github.com/victor95pc/jest-react-h...