Rules of Hooks: Only Call Hooks from React Functions
If you need to test a custom Hook, you can do so by creating a component in your test, and using your Hook from it. Then you can test the component you wrote. (copied).
Even though hooks 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.
In some cases, hooks may not even return a value, so we have to test components that use hooks.
As react-hooks-testing-library
said
-
When to
- You're writing a library with one or more custom hooks that are not directly tied a component
- You have a complex hook that is difficult to test through component interactions
-
When not to
- Your hook is defined along side a component and is only used there
- Your hook is easy to test by just testing the components using it
If you are one of the following, please read on.
- you don't know how to test a component
- you want to reduce the boilerplate
- you have a complex hook that is difficult to test through component interactions
- packages/react-reconciler/src/tests/ReactHooks-test.internal.js
- packages/react-reconciler/src/tests/ReactHooksWithNoopRenderer-test.internal.js
Most of the libraries here are for testing components, not for testing the return value of hooks.
But We can write a helper function that exposes the result of hooks from inside the component, I will show that by enzyme
.
- Basic: react-dom/test-utils
- Component level: Enzyme, React Testing Library
- Hook level: react-hooks-testing-library
I will use following useCounter
to show how to test.
import { useState, useCallback } from 'react';
function useCounter() {
const [count, setCount] = useState(0);
const increment = useCallback(() => setCount((x) => x + 1), []);
return { count, increment };
}
export default useCounter;
the easiest and most verbose solution, don't need to intro any libraries. official example
import React from 'react';
import ReactDOM from 'react-dom';
import { act } from 'react-dom/test-utils';
import useCounter from '../useCounter';
function Counter() {
const { count, increment } = useCounter();
return (
<button onClick={increment}>{count}</button>
);
}
let container;
beforeEach(() => {
container = document.createElement('div');
document.body.appendChild(container);
});
afterEach(() => {
document.body.removeChild(container);
container = null;
});
it('should increment counter', () => {
// Test first render and effect
act(() => {
ReactDOM.render(<Counter />, container);
});
const button = container.querySelector('button');
expect(button.textContent).toBe('0');
// Test second render and effect
act(() => {
button.dispatchEvent(new MouseEvent('click', { bubbles: true }));
});
expect(button.textContent).toBe('1');
});
This solution is similar to react-dom/test-utils
, just in order to reduce the boilerplate.
import React from 'react';
import { render, fireEvent, waitForElement } from '@testing-library/react';
import useCounter from '../useCounter';
function Counter() {
const { count, increment } = useCounter();
return (
<button data-testid="button" onClick={increment}>{count}</button>
);
}
it('should increment counter', () => {
const { getByTestId } = render(<Counter />);
const button = getByTestId('button');
expect(button.textContent).toBe('0');
fireEvent.click(button)
expect(button.textContent).toBe('1');
});
- Test component
This solution is similar to react-dom/test-utils
as well.
import React from 'react';
import { shallow } from 'enzyme';
import useCounter from '../useCounter';
function Counter() {
const { count, increment } = useCounter();
return (
<button onClick={increment}>{count}</button>
);
}
it('should increment counter', () => {
const wrapper = shallow(<Counter />);
expect(wrapper.find('button').text()).toBe('0');
wrapper.find('button').simulate('click');
expect(wrapper.find('button').text()).toBe('1');
});
- Test returns of hooks
This demo can only cover simple scenarios, see the next demo for a more robust solution.
import React from 'react';
import { shallow } from 'enzyme';
import useCounter from '../useCounter';
it('should increment counter', () => {
let count;
let increment;
function Counter() {
({ count, increment } = useCounter());
return count;
}
shallow(<Counter />);
expect(count).toBe(0);
increment();
expect(count).toBe(1);
// const wrapper = shallow(<Counter />);
// expect(wrapper.text()).toBe('0');
// increment();
// expect(wrapper.text()).toBe('0');
});
Obviously, we can abstract out the render logic
import React from 'react';
import { shallow } from 'enzyme';
import useCounter from '../useCounter';
function renderHook(hook) {
let result = { current: null };
function Counter() {
const currentResult = hook();
result.current = currentResult;
return null;
}
shallow(<Counter />);
return result;
};
it('should increment counter', () => {
const result = renderHook(useCounter);
expect(result.current.count).toBe(0);
result.current.increment();
expect(result.current.count).toBe(1);
});
- You're writing a library with one or more custom hooks that are not directly tied a component
- You have a complex hook that is difficult to test through component interactions
- Your hook is defined along side a component and is only used there
- Your hook is easy to test by just testing the components using it
import { renderHook, act } from '@testing-library/react-hooks';
import useCounter from '../useCounter';
it('should increment counter', () => {
const { result } = renderHook(() => useCounter());
expect(result.current.count).toBe(0);
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
yarn test
- How to test components that use Hooks?
- react-testing-library
- react-hooks-testing-library
- Article: React Hooks: What's going to happen to my tests?
- Article: React Hooks: Test custom hooks with Enzyme
MIT