Write Unit Test For React Application. (vitest, Testing-library)

Arjun DangiArjun Dangi
7 min read

Here we will use the following packages for writing unit tests. Our application will be a react application that uses Tanstak-react-query for API calls.

  1. Vitest

  2. Testing-library

  3. Happy-dom

  4. Mock Server Worker (MSW)

Testing API Calls

In modern web applications, API calls are crucial for fetching and displaying data. Unit testing API calls ensures that your application interacts with APIs correctly and handles responses as expected.

When writing unit tests for API functionality, it's common practice not to call the actual APIs but instead use mock servers or mock functions to simulate API responses. Here are the reasons why:

Isolation for Predictable Testing

By using mock servers or functions, we can isolate our tests from external dependencies like actual APIs. This isolation ensures that our tests remain predictable and are not affected by changes or fluctuations in the real API's behavior.

Controlled Testing Scenarios

Mocking API responses allows us to control the scenarios we want to test. We can simulate different responses such as success, failure, or edge cases without relying on the actual API's availability or specific data states.

Mock servers and functions provide a controlled and efficient way to test API interactions in unit tests, ensuring that our code behaves as expected under different scenarios without relying on external services.

Mock Server Worker (MSW) Setup

Mock Server Worker (MSW) is a powerful tool for mocking API requests and responses during testing. Setting up MSW involves creating a mock service folder and defining mock handlers for API endpoints.

Create a mock service folder in the src directory. Then create a server.js file in that folder. You can create handlers.js as well here.
Reference msw/gettings/tarted

import { setupServer } from 'msw/node';
export const server = setupServer();

Now Let's create a page.jsx that makes an API call and displays data on UI.

import { getBooks } from '../lib/api/books';

const {data} = useQuery({
    queryKey: ['books'],
    queryFn: () => getBooks(),
  });

return (
 <h1> Display Data </h1>
  {data?.length > 0 ? (   
    {
      data?.map((book)=> 
       <p data-testid='book-component' > {book?.name} </p> 
     )}
  ) : (
     <p data-testid='no-data' > No Books Available </p>
  )}

Setting Up Mock Server with server.listen()

To begin, we use the beforeAll hook provided by vitest to set up our mock server using server.listen(). This step ensures that our mock server starts intercepting requests before any tests are executed, allowing us to control API responses.

javascriptCopy codebeforeAll(() => server.listen());

Cleaning Up After Each Test with afterEach

After each test case, we want to reset our mock server handlers, clean up any rendered components, and reset test utilities to maintain a clean testing environment. The afterEach hook, combined with server.resetHandlers() and cleanup(), handles these cleanup tasks effectively.

javascriptCopy codeafterEach(() => {
  server.resetHandlers();
  cleanup();
});

Closing Mock Server After All Tests with server.close()

Once all tests are finished, we use the afterAll hook to close our mock server using server.close(). This step stops the mock server from intercepting requests and cleans up any resources used during testing.

javascriptCopy codeafterAll(() => server.close());

Writing the Test Case for Fetching and Displaying Books

The actual test case involves mocking the API response for fetching books, rendering the application, and asserting that the books are displayed correctly on the UI.

Test 1:- Should fetch and display books on UI

//book.test.jsx

import React from 'react';
import { describe, test, expect, beforeAll, afterEach, afterAll } from 'vitest';
import { cleanup, render, screen, waitFor } from '@testing-library/react';
import App from '../../../App';
import { server } from '../../../../mockServices/server';
import { http, HttpResponse } from 'msw';


describe('books', () => {
  beforeAll(() => server.listen());
  afterEach(() => {
    server.resetHandlers();
    cleanup();
  });
  afterAll(() => server.close());

test('Should fetch and display books on UI', async () => {
    //mocking the api response
    server.use(
      http.get(`https://api.book.com/api`, () => {
        return HttpResponse.json({
          events: [{name:'book1',price:200}],
        });
      }),
    );

    render(<App />);
    //by using screen.
    const books = await screen.findAllByTestId('book-component');
    expect(books).toHaveLength(1);

  });

}

Test 2:- Should display No Books Available when data length is zero in the success response

test('should display No data in success response fetched zero books', async () => {
    server.use(
      http.get(`https://api.book.com/api`, () => {
        return HttpResponse.json({
          events: [],
        });
      }),
    );
    const { findByTestId, queryByTestId } = render(<App />);
    //you can use screen.queryByTestId/findByTestId as well
    await waitFor(() => expect(queryByTestId('book-component')).toBeFalsy());
    expect(findByTestId('no-data')).toBeTruthy();
  });

Testing Navigation between pages

Navigating between pages is a common functionality in web applications. Testing navigation ensures that users can move seamlessly within the app.

Mocking useNavigate with vi.mock

To test navigation, we mock the useNavigate hook provided by react-router using vi.mock. This allows us to control the navigation behavior within our test environment without actually navigating.

import { useNavigate } from 'react-router';
const navigate = useNavigate();

 const onClick = () => {
  navigate('/setup');
};

<button 
 data-testid='navigate-buttton'  
 onClick={onClick} 
> Click  </button>
//button.test.jsx 

const mockedUsedNavigate = vi.fn();  //keep it at top of file

//Here we are mocking partially means only useNavigate we are mocking and remaining we are required actual.
beforeEach(() => {
    vi.mock('react-router', () => {
      return {
        ...vi.importActual('react-router'),  // for vitest 
        ...jest. requireActual('react-router') // for jest
        useNavigate: () => mockedUsedNavigate,
      };
    });
  });

  describe('...........', () => {
          test('.........'), async()=>{
            render(<App/>)
            const button = await findByTestId('navigate-buttton');
            expect(button).toBeTruthy();

           fireEvent.click(button);
           await waitFor(() => expect(mockedUsedNavigate).toBeCalledWith('/path'));

           //if want to check navigate should not happen 
           await waitFor(() => expect(mockedUsedNavigate).not.toBeCalledWith('/another path));
      }
  }

Testing Specific page/component

When testing a specific page or component in isolation, we use tools like MemoryRouter to simulate routing without rendering the entire application. This is useful when you want to focus your test on a particular part of the application, such as a single page or a specific component.

Why Testing Specific Page/Component?

  • Isolated Testing: Focus solely on the behavior and functionality of a specific page or component without rendering the entire application.

  • Efficiency: Speed up testing by avoiding rendering unnecessary parts of the application that are not relevant to the test scenario.

  • Modular Testing: Test individual components or pages independently to ensure they work correctly in isolation and when integrated into the larger application.

Using MemoryRouter for Isolated Testing

MemoryRouter is a component provided by react-router-dom that allows us to simulate routing in a testing environment without affecting the actual browser URL. When used in tests, MemoryRouter creates a virtual DOM for routing, ensuring that routing-related components and functionalities are tested without triggering real UI changes or browser navigation.

const HomePage = () => {
  return <div data-testid='home-page' >
     <h1>Home Page<h1/>
 </div>;
};

  test('should test page is rendered', async () => {

    const pathName = '/home'// route name where button component is mount
    const { getByTestId } = render(
      <MemoryRouter initialEntries={[pathName]}>
        <HomePage />
      </MemoryRouter>,
    );

    const HomePage = getByTestId('home-page');
    expect(HomePage).toBeTruthy();

  });

In this example, MemoryRouter is used to render the HomePage component with a specified route (/home) without affecting the actual browser URL. This allows us to test the rendering of the home page component in isolation, ensuring that it renders correctly without triggering real UI changes or navigation.

By utilizing MemoryRouter in tests, we can isolate specific pages or components for testing purposes without impacting the overall application state or UI.

Testing Function call

Testing function calls ensures that functions within components are invoked correctly in response to user interactions or other events. This type of testing validates the behavior of interactive elements and the flow of data and actions within the application.

Why Testing Function Calls?

  • User Interaction: Verify that user interactions, such as button clicks, trigger the intended functions.

  • Data Flow: Test the flow of data and actions within components to ensure proper functionality.

  • Event Handling: Validate that event handlers are correctly bound and executed as expected.


const ButtonComponent = ({ onClickFuntionFromParent }) => {
  const handleClick = () => {
    onClickFuntionFromParent()
  };
  return (      
    <button
    onClick={handleClick}
    data-testid='button'
    > Click here  </button>    
  );
};

  test('should invoke function call', async () => {
    const mockFunction = vi.fn();
    const pathName = ''// route name where button component is mount
    const { getByTestId } = render(
      <MemoryRouter initialEntries={[pathName]}>
        <ButtonComponent onClickFuntionFromParent={mockFunction} />
      </MemoryRouter>,
    );

    const button = getByTestId('button');
    expect(button).toBeTruthy();
    fireEvent.click(button);
    expect(mockFunction).toBeCalled();
  });

Errors

  1. toBeInTheDocument is not a function

     //we have to import jest-dom at top in test file
     import "@testing-library/jest-dom";
    
0
Subscribe to my newsletter

Read articles from Arjun Dangi directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Arjun Dangi
Arjun Dangi