DEV Community

Vladislav Zimnikov
Vladislav Zimnikov

Posted on • Edited on

Testing modern ReactJS Application: Unit Testing(Part 2)

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

  1. Class vs. Function
  2. Testing React Components
    1. Experiment Subjects
    2. Rendering Result
    3. Asynchronous Rendering Result
  3. Hook testing

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>
    );
};
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

Next we use this render function to render component the way we need. Simply pass JSX as argument

render(
            <TestComponent>
                <div>Node</div>
            </TestComponent>
        );
Enter fullscreen mode Exit fullscreen mode

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')
Enter fullscreen mode Exit fullscreen mode

And to actually check that we found it, let's expect that resulted value is defined

expect(screen.getByText('Node')).toBeDefined();
Enter fullscreen mode Exit fullscreen mode

If we run script test we should see successful output. In case you need to update snapshots don't forget to add -u flag

first yarn test result testing rendering result

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';
Enter fullscreen mode Exit fullscreen mode

And now replace matcher toBeDefined with the one called toBeInTheDocument

expect(screen.getByText('Node')).toBeInTheDocument();
Enter fullscreen mode Exit fullscreen mode

Rerun script test and check if testing passes

script test result with matcher toBeInTheDocument

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>
    );
};
Enter fullscreen mode Exit fullscreen mode

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();
    });
Enter fullscreen mode Exit fullscreen mode

Script test result:

async rendering 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();
Enter fullscreen mode Exit fullscreen mode

Script test result:

second async rendering 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];
}
Enter fullscreen mode Exit fullscreen mode

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>
    );
};
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

yarn:

yarn add -D @testing-library/react-hooks
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

Full test case looks like that

const { result, waitForNextUpdate } = renderHook(() => useMockRequest(100));

expect(result.current[0])
    .toBeTruthy();

await waitForNextUpdate();

expect(result.current[0])
    .toBeFalsy();
Enter fullscreen mode Exit fullscreen mode

Run script test to invoke all tests

result of yarn test after adding test for hook

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)