As I promised in previous post in this part I would like to tell about unit testing of components, difference between testing functional and class components and how to test react hooks
Table of contents
Class vs. Function
As you may know ReactJS offers two ways of writing components: class-based and function-based. Latter approach offered more concise way of writing components and in the meantime enabled usage of React Hooks
In terms of testing there is significant difference between classes and functions. Functions, defined inside of function components cannot be mocked. If for some reason you want to have possibility to mock any of methods used in your component consider using class-based approach
In my opinion, this limitation is not limitation at all since React components represent some parts of User Interface and therefore should not be tested the same way we test backend code. You'll get what I mean a bit later
Testing React Components
Experiment Subjects
Before writing any tests we need few components to test. In the beginning of each section I will provide content of component I'm going to test. You are free to use any other component for experimenting
Rendering Result
Component to test:
import React from 'react';
export default function TestComponent({ children }) {
return (
<div>
{ children }
</div>
);
};
When it comes to testing rendering output we can't rely on snapshots since they meant for regression testing
Regression testing is a software testing practice that ensures an application still functions as expected after any code changes, updates, or improvements
When we need to ensure that component renders properly on given input(or without it) @testing-library/react
steps in
Firstly, we will need to render component. For this to manage we need to import render
function from @testing-library/react
. Component will be rendered in artificial DOM. To easily find nodes in this DOM we will import screen
object
import { render, screen } from '@testing-library/react';
Next we use this render
function to render component the way we need. Simply pass JSX as argument
render(
<TestComponent>
<div>Node</div>
</TestComponent>
);
Now we can use queries provided by testing-library
in screen
object. As React components are about building User Interface that is presented to end user those queries provide methods to find nodes the way users see them. It becomes more clear when you see it in action
Now we expect to see node with text Node
. Let's literally try to find such element. We can do it following way
screen.findByText('Node')
And to actually check that we found it, let's expect that resulted value is defined
expect(screen.getByText('Node')).toBeDefined();
If we run script test
we should see successful output. In case you need to update snapshots don't forget to add -u
flag
But currently our new test suite is not self-descriptive and informative. Library @testing-library/jest-dom
provide many additional matchers for DOM nodes. Import it into test file
import '@testing-library/jest-dom';
And now replace matcher toBeDefined
with the one called toBeInTheDocument
expect(screen.getByText('Node')).toBeInTheDocument();
Rerun script test
and check if testing passes
Asynchronous Rendering Result
It's very common when component performs request to API and waits for response before rendering final result. Firstly, adjust TestComponent to mock server request and append conditional rendering
import React, { useEffect, useState } from 'react';
export default function TestComponent({ children }) {
const [isLoading, setIsLoading] = useState(true);
useEffect(() => setTimeout(() => setIsLoading(false), 100), []);
if (isLoading) {
return (
<div>Loading</div>
);
}
return (
<div>
{ children }
</div>
);
};
I'll use useState
and useEffect
hooks combined with setTimeout to postpone state change
Now since our component renders result not instantly, different query should be used. Queries provided by testing-library
that allow to work with asynchronous rendering start with find
prefix instead of get
we used previously for synchronously rendered content
Important notice: find
queries waits up to 1000ms
Make test suite's callback async
, replace query with findByText
and await on returned Promised. Looks like following
it('should render properly', async () => {
render(
<TestComponent>
<div>Node</div>
</TestComponent>
);
expect(await screen.findByText('Node'))
.toBeInTheDocument();
});
Script test
result:
Now let's also make sure that Loading
node is rendered initially. Simply use query getByText
to look for node containing Loading
text before last expect
were we await until final result is rendered
expect(screen.getByText('Loading'))
.toBeInTheDocument();
expect(await screen.findByText('Node'))
.toBeInTheDocument();
Script test
result:
More info on queries provided by testing-library
Hook testing
I'll write simple hook that mocks request to server the same way I did it previously using setTimeout to add artificial delay
export function useMockRequest(delay) {
const [isLoading, setIsLoading] = useState(true);
useEffect(() => setTimeout(() => setIsLoading(false), delay), []);
return [isLoading];
}
TestComponent file:
import React, { useEffect, useState } from 'react';
export function useMockRequest(delay) {
const [isLoading, setIsLoading] = useState(true);
useEffect(() => setTimeout(() => setIsLoading(false), delay), []);
return [isLoading];
}
export default function TestComponent({ children }) {
const [isLoading] = useMockRequest(100);
if (isLoading) {
return (
<div>Loading</div>
);
}
return (
<div>
{ children }
</div>
);
};
Firstly, let's add new describe
block to our test file and give it a title useMockRequest
. Create one test inside of new describe
for further usage. I will name it should change state after given delay
In real world application you would create new file for new test block but currently I want to keep it simple
Now let's clarify what are React Hooks.
React Hooks are functions that enable control on your component's behavior. When it comes to testing it can be a bit misleading since you cannot really use capabilities of React hooks outside of a component. Or can we?
testing-library
provides one more library exactly for such purpose allowing us to avoid headache and safe strength for actual testing. It is called @testing-library/react-hooks
Let's add it as development dependency
npm:
npm install -D @testing-library/react-hooks
yarn:
yarn add -D @testing-library/react-hooks
It provides a lot of tools for easy and comfortable hook testing but let's check them step-by-step
First thing that needs to be done is hook rendering. Our new library will do all hard work itself. See how it looks like below
import { renderHook } from '@testing-library/react-hooks';
...
const result = renderHook(() => useMockRequest(100));
To render hook we need renderHook
. Pretty straightforward, isn't it?
Then you call it and pass callback as argument inside of which you invoke your hook with or without arguments
The result of invocation is an object that provides many fields and utility functions to proceed with rendered hook testing
First thing we need to get is the actual result of hook invocation since we have to verify that initial state is equal to true
. Hook's return value can be accessed by result
field of an object returned by renderHook
function. I will utilise destructuring to keep code concise
const { result } = renderHook(() => useMockRequest(100));
Object result
also contains multiple fields but we should be interested in current
as it is containing exactly what we need
Since our hook returns array of two elements, current
property will be exactly this array. To validate that state, returned by hook initially is false, just access first element of current
property and add assertion on that
expect(result.current[0])
.toBeTruthy();
First state is tested, next thing that should be checked is that state changes after some time and to achieve that we need to wait for hook to rerender. Exactly for this purpose renderHook
returns function called waitForNextUpdate
To wait for next hook update we have to... await
a Promise this function returns
await waitForNextUpdate();
Be aware that by default it waits up to 1000ms but it can be changed by passing additional argument
Once promise had been awaited, we can check absolutely the same value to be changed - the one inside of result.current[0]
. Now we expect it to be false
expect(result.current[0])
.toBeFalsy();
Full test case looks like that
const { result, waitForNextUpdate } = renderHook(() => useMockRequest(100));
expect(result.current[0])
.toBeTruthy();
await waitForNextUpdate();
expect(result.current[0])
.toBeFalsy();
Run script test
to invoke all tests
This was only top of the mountain in regards of testing hooks. I'll dive deeply into this topic in separate post or series of posts
Here is the GitHub repo with all my code in one place if you need
In addition, feel free to leave a comment about what did you like and what did you not
In regards of this post this is it for today. See you next time!
Top comments (0)