React Testing using Vitest and React Testing Libraries (Part II)

Sujan
by Sujan 

This is part two of our blog series on react testing. Read the first part here. In this second part, I will focus on the following:

  • What to Test
  • User Interactions
  • Hook Test

What to test?

Before writing the test we must consider what should be tested and what shouldn’t. We have to make sure that only the required test should be written and tested. Writing unnecessary tests is a waste of time and effort, also it takes a decent amount of time to run a test and would lead to an increase in time for CI/CD pipelines.

Stuff that should be tested: 

  • Test component renders
  • Test component renders with props
  • Test component renders in different states
  • Test component reacts to events, (User interaction)

Stuff that should not be tested:

  • Third-party code
  • Implementation details
  • Code that is not important from a user’s point of view

User Interaction

The test must be able to simulate user interaction to understand the user flow and behave accordingly. An event such as a click using a mouse or a keyboard keypress should be responded to and ensure the interactions are handled as expected.

For that, we will be using a library called user-event, a companion library for Testing Library that simulates user interactions by dispatching the events that would happen if the exchange took place in a browser.

Writing component for the interaction test

import React from "react";

function App() {
  const [count, setCount] = React.useState(0);
  const [amount, setAmount] = React.useState(0);

  return (
    <>
      <p> count is {count}</p>

      <input
        type="number"
        onChange={(e) => setAmount(Number(e.target.value))}
        name="amount"
        value={amount}
      />

      <button onClick={() => setCount(amount)}>Set</button>
    </>
  );
}

export default App;

Writing test code for UI interaction

import { render, screen } from "@testing-library/react";
import user from "@testing-library/user-event";

import App from "./App";

descibe("UI interaction", () => {

	const amt = 10;

	// Test 1
  test(`Amount Input field should have initial value 0`,  () => {
    user.setup();
    render(<App />);
		// spinbutton is the role for the input field of type number
    const amountInput = screen.getByRole("spinbutton");

		expect(amountInput).toBeInTheDocument();
		// check if the initial value is 0
		expect(amountInput).toHaveValue(0);

	  const setBtn = screen.getByRole("button", { name: /set/i });
		expect(setBtn).toBeInTheDocument();

	})

	// Test 2
	// Test with user interaction should be async
  test(`set counter to ${amt} after ${amt} is set`, async () => {
    user.setup();
    render(<App />);

    const amountInput = screen.getByRole("spinbutton");
	
		// typing amt (10) into amountInput
    await user.type(amountInput, `${amt}`);
		// expecting to have a value 10
    expect(amountInput).toHaveValue(amt);

    const setBtn = screen.getByRole("button", { name: /set/i });
		// Clicking the setBtn
    await user.click(setBtn);

    const countEle = screen.getByText(/count is/);
		// expecting the count state to have a value of 10
    expect(countEle).toHaveTextContent(`${amt}`);
  });
	
})

Test Code Explanation

In the above code, Test 1, is similar to the one in part 1, its amountInput and setBtn to be in the document, additionally it also checks if the amountInput has the initial value of 0.

In Test 2, we use the user feature from user-events. the user is able to perform all the interactions that a real user can do in a browser. First, we type 10 in the amountInput field, then we expect to have the typed value.

After that, we click on the setBtn, this trigger function to set the count of the amountInput. So we expect the count state to have a value of 10.

If the test satisfies the above conditions the test passes , otherwise it fails.

Testing Hooks

Hooks are one the most used features in a React application. In a project there might be multiple custom hooks that we create and use in multiple parts of our project. Thus we need to test our custom hooks. Here we will be creating a custom hook for the counter and writing a test code for it.

Writing a hook for counter

import { useState } from "react";

type propsT = {
  initial?: number;
};

const useCounter = ({ initial = 0 }: propsT = {}) => {
  const [count, setCounter] = useState(initial);

  const increment = () => setCounter((counter) => ++counter);
  const decrement = () => setCounter((counter) => --counter);

  return { count, increment, decrement };
};

export default useCounter;

Writing test codes for the counter hook

import { renderHook, act } from "@testing-library/react";

import useCounter from "./useCounter";

describe("useCounter", () => {
  const initialCount = 10;

	// Test 1
  test("should return the initial count", () => {
		// Here we use renderhook func to render the hook instead of just render
    const { result } = renderHook(useCounter);

		// Need to destructure the result and get the current to access 
		// the returned items, form hook
    expect(result.current.count).toBe(0);
  });

	// Test 2
  test("should set the count to initial count", () => {
		// For passing the props we need to define initialProps attribute
    const { result } = renderHook(useCounter, {
      initialProps: { initial: initialCount },
    });
    expect(result.current.count).toBe(initialCount);
  });

	// Test 3
  test("should increment the count", () => {
    const { result } = renderHook(useCounter, {
      initialProps: { initial: initialCount },
    });
		// Need to wrap the function call with act function
    act(() => result.current.increment());
    expect(result.current.count).toBe(initialCount + 1);
  });

	// Test 4
  test("should decrement the count", () => {
    const { result } = renderHook(useCounter, {
      initialProps: { initial: initialCount },
    });
    act(() => result.current.decrement());
    expect(result.current.count).toBe(initialCount - 1);
  });
});

Test code explanation

Here in the first line, we can see that instead of importing screen and render, we have imported renderHook and act from the “@testing-library/react”. As we must use hooks only in the functional components, the testing-library provides a special function to render hooks.

In Test 1, we check if the initial state of the count (without props passing) is as expected or not, for this we check the value of the count which we get by destructing the result ( result.current.count ), from the render of the hook.

In Test 2, we check if the initial state of the count (with props passing) is as expected or not, we can pass in the props by adding initial props attributes, in the second param of the renderHook.

In Test 3 and Test 4, we check the increment and decrement function provided by our custom hook, to check the function we need to call the function by wrapping it under the act function. If the count is increased and decreased with the invocation of the respective function, our test is passed.

Final Words

Gurzu is a full-cycle Software development company. Since 2014, we have built softwares for many startups and enterprises from all around the world using Agile methodology. Our team of experienced developers, designer, test automation engineers can help to develop your next product.

Read more about our services here. Have a tech idea you want to turn into reality? Book a free consulting call.