Write Unit Test For React Application. (vitest, Testing-library)
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.
Vitest
Testing-library
Happy-dom
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
toBeInTheDocument is not a function
//we have to import jest-dom at top in test file import "@testing-library/jest-dom";
Subscribe to my newsletter
Read articles from Arjun Dangi directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by