Now you can use shallow rendering for testing React components with hooks. And a few words on why shallow rendering is good.
Up until recently it was tricky to use shallow rendering and libraries like enzyme
for testing React components that relied on hooks like useEffect()
and useLayoutEffect()
. So I've released a library - jest-react-hooks-shallow - that brings those hooks to shallow rendering.
All you need to do is to download the library:
npm install --save-dev jest-react-hooks-shallow
# or
yarn add --dev jest-react-hooks-shallow
and add these lines to your Jest setup file (specified by setupFilesAfterEnv
):
import enableHooks from "jest-react-hooks-shallow";
// pass an instance of jest to `enableHooks()`
enableHooks(jest);
And voilà - useEffect()
and useLayoutEffect()
will work with shallow rendering. From this moment on your test don't need to know anything about useEffect()
. After all, it's a mere implementation detail.
So if you have a component like this:
const ComponentWithHooks = () => {
const [text, setText] = useState<>();
const [buttonClicked, setButtonClicked] = useState<boolean>(false);
useEffect(() => setText(
`Button clicked: ${buttonClicked.toString()}`),
[buttonClicked]
);
return (
<div>
<div>{text}</div>
<button onClick={() => setButtonClicked(true)}>Click me</button>
</div>
);
};
You can easily test it with code like this:
test("Renders default message and updates it on clicking a button", () => {
const component = shallow(<App />);
expect(component.text()).toContain("Button clicked: false");
component.find("button").simulate("click");
expect(component.text()).toContain("Button clicked: true");
});
Please note, that those tests didn't have to import anything else. They simply don't know that a component calls useEffect()
. Yet, it's being called when you invoke shallow()
.
That said, often you want to test that a specific function has been called on some event. For example, you're calling a Redux action creator or a Mobx action. If you're using React Hooks, chances are you'll pass that function as a callback to useEffect()
.
No problems! You can easily test it with simple Jest mocks.
Say, we have a component like this:
import someAction from './some-action';
const ComponentWithHooks = () => {
const [text, setText] = useState<>();
const [buttonClicked, setButtonClicked] = useState<boolean>(false);
useEffect(someAction, [buttonClicked]);
return (
<div>
<div>{text}</div>
<button onClick={() => setButtonClicked(true)}>Click me</button>
</div>
);
};
test('Calls `myAction()` on the first render and on clicking the button`', () => {
const component = shallow(<App />);
expect(callback).toHaveBeenCalledTimes(1);
component.find('button').simulate('click');
expect(callback).toHaveBeenCalledTimes(2);
});
You can find out more about jest-react-hooks-shallow
on its Github page.
Some people may say why bring React Hooks to enzyme when there's a trend to use full rendering with libraries like react-testing-library
. I've even sparked an interesting discussion on that when I posted about jest-react-hooks-shallow
on Reddit. You may check these two sub-threads: one and two.
So there are a few good reasons for doing shallow rendering:
Let's say you have the following component hierarchy:
ComponentA -> ComponentB -> ComponentC (makes an HTTP request)
And you're writing a unit test for ComponentA
. If you render the entire component tree, you tests may not work as expected because of the HTTP request made by ComponentC
.
So you either have to mock component B
- and that would be very similar to doing shallow rendering. Or you would have to mock component C
or provide a stub backend. But the last two options are hardly ideal because they break encapsulation. Your component A
has no knowledge of component C
or any HTTP requests, why would a test for that component require that knowledge?
Shallow rendering also assists with test-driven development. Let's take a previous example, but imagine that component A
doesn't exist, but you have to write, because you need to wrap component B
in another component. So it'll be far easier to write tests first for a new component that renders the existing ones, when you don't have to render the entire tree.
If you have comprehensive unit tests for your components that don't rely rendering the whole tree, it'll be easier make such components re-usable and even extract them to stand-alone libraries.
There are two popular misconceptions about shallow rendering:
First of all, it is absolutely true that it is bad to test implementation details and you should test from a user's point of view.
But shallow rendering does not force use to test implementation details. And it does allow you to test from a user's point of view.
There's a famous example of reading and setting React state in unit tests. This is wrong. You don't have to that and you can easily test without it.
Also, testing that your component renders specific child components or passes specific properties is testing implementation details, it is actually testing its behaviour. After all, that's what your component does - it renders certain elements on certain conditions and passes data to other components.
Let's have a look at a few examples on how you can test components that have different behaviour:
const MyComponent = () => <div>My message</div>;
it("Renders message", () => {
const component = shallow(<MyComponent />);
expect(component.text()).toContain("My message");
});
true
, then you need to test that it renders that component when the property is true
and it doesn't when it is false
const MyComponent = ({ displayChild }) => (
<>{displayChild && <ChildComponent />}</>
);
it("Renders `ChildComponent` when necessary", () => {
expect(
shallow(<MyComponent displayChild={false} />).find(ChildComponent)
).toHaveLength(0);
expect(
shallow(<MyComponent displayChild={true} />).find(ChildComponent)
).toHaveLength(1);
});
const MyComponent = () => {
cost[(displayChild, setDisplayChild)] = useState(true);
return (
<>
{displayChild && <ChildComponent />}
<button onClick={() => setDisplayChild(false)}>Hide child</button>
</>
);
};
it("Hides `ChildComponent` after pressing on the button", () => {
const component = shallow(<MyComponent />);
expect(component.find(ChildComponent)).toHaveLength(0);
component.find("button").simulate("click");
expect(component.find(ChildComponent)).toHaveLength(1);
});
The last example perfectly illustrates how you can test components from a user point of view and still use shallow rendering.
const MyComponent = () => {
cost[(accepted, setAccepted)] = useState(false);
return (
<>
<button onClick={() => setAccepted(true)}>Accept</button>
<ChildComponent accepted={accepted} />
</>
);
};
it("Passes `accepted` to `ChildComponent` on pressing the button", () => {
const component = shallow(<MyComponent />);
expect(component.find(ChildComponent).prop("accepted")).toBeFalse();
component.find("button").simulate("click");
expect(component.find(ChildComponent).prop("accepted")).toBeTrue();
});
Finally, if you really want to test from a user's standpoint, then make sure that you have a few end-to-tests. They could be time consuming to write and run. But at they can tests the whole system end-to-end including the backend.
enzyme
for testing React components with hooks
The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way. My personal thoughts tend to change, hence the articles in this blog might not provide an accurate reflection of my present standpoint.
© Mike Borozdin