Vitest: how to test React with tests that truly help
Tests are not optional. They are essential for ensuring software quality and confidence in the functionalities we are implementing. Especially in React applications, where user interactions and dynamic behavior are constant. Here, I will share how to set up and use Vitest to efficiently test your React applications, ensuring that your tests genuinely help improve the quality of your code.
## Setup with Vite
To get started, we need to set up our environment. Vitest integrates seamlessly with Vite, which is a modern project build tool. To begin, create a new project using Vite if you don't have one yet:
```bash
npm create vite@latest my-project --template react
cd my-project
npm install
Now, let's add Vitest:
npm install -D vitest @testing-library/react @testing-library/jest-dom
Next, we need to configure Vitest. Create a file vite.config.js at the root of the project with the following basic configuration:
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: 'jsdom',
},
});
With this, we already have Vitest ready for testing. Now, create a folder src/__tests__ where we will store our test files.
```markdown
## Unit tests with React Testing Library
The React Testing Library is an excellent tool for testing React components, as it focuses on how users interact with the interface. Let's start by writing a simple test for a component.
Suppose we have a component `Button.js`:
```javascript
import React from 'react';
const Button = ({ label, onClick }) => {
return <button onClick={onClick}>{label}</button>;
};
export default Button;
Now, let's create a test for it. Create a file named Button.test.js inside the __tests__ folder:
import { render, screen, fireEvent } from '@testing-library/react';
import Button from '../Button';
test('renders button with correct label', () => {
render(<Button label="Click here" onClick={() => {}} />);
const buttonElement = screen.getByText(/click here/i);
expect(buttonElement).toBeInTheDocument();
});
test('calls onClick when clicked', () => {
const handleClick = jest.fn();
render(<Button label="Click here" onClick={handleClick} />);
const buttonElement = screen.getByText(/click here/i);
fireEvent.click(buttonElement);
expect(handleClick).toHaveBeenCalledTimes(1);
});
These are examples of how we can verify that the button is being rendered correctly and that the onClick function is called when we click on it.
```markdown
## Mocking and Spies
Mocking is an essential technique for isolating parts of your code during testing. Vitest has built-in support for mocks. Let's see how we can mock a function.
Suppose we have a file `api.js` that makes an API call:
```javascript
export const fetchData = async () => {
const response = await fetch('https://api.example.com/data');
return response.json();
};
We can mock this function in our test:
import { fetchData } from '../api';
import { vi } from 'vitest';
test('fetchData calls the API and returns data', async () => {
const mockData = { items: ['item1', 'item2'] };
global.fetch = vi.fn(() =>
Promise.resolve({
json: () => Promise.resolve(mockData),
})
);
const data = await fetchData();
expect(data).toEqual(mockData);
expect(global.fetch).toHaveBeenCalledTimes(1);
});
Here, we are using vi.fn() from Vitest to create a mock of the fetch function, allowing us to test fetchData without making a real API call.
```markdown
## Integration Tests
Integration tests verify how different parts of the system work together. Let's create a simple example where a component fetches data from an API and displays it.
Suppose we have a component `DataDisplay.js`:
```javascript
import React, { useEffect, useState } from 'react';
import { fetchData } from './api';
const DataDisplay = () => {
const [data, setData] = useState(null);
useEffect(() => {
const getData = async () => {
const result = await fetchData();
setData(result);
};
getData();
}, []);
if (!data) return <div>Loading...</div>;
return (
<div>
{data.items.map((item, index) => (
<div key={index}>{item}</div>
))}
</div>
);
};
export default DataDisplay;
Now, let's test this component:
import { render, screen } from '@testing-library/react';
import DataDisplay from '../DataDisplay';
import { fetchData } from '../api';
import { vi } from 'vitest';
vi.mock('../api');
test('renders loading state and fetches data', async () => {
const mockData = { items: ['item1', 'item2'] };
fetchData.mockResolvedValue(mockData);
render(<DataDisplay />);
expect(screen.getByText(/loading/i)).toBeInTheDocument();
const itemElements = await screen.findAllByText(/item/i);
expect(itemElements).toHaveLength(2);
});
In this test, we are mocking the fetchData function again and checking if the component displays the loading state before rendering the items.
```markdown
## Coverage and CI
Test coverage is crucial for understanding which parts of your code are being tested. Vitest provides support for easily generating coverage reports. To enable coverage, you can add the following configuration to your `vite.config.js`:
```javascript
test: {
coverage: {
reporter: ['text', 'json', 'html'],
},
},
Now, when you run the tests, you will see a detailed coverage report. For continuous integration, you can use tools like GitHub Actions or GitLab CI to ensure that your tests are run on every pull request, validating the quality of the code before it is merged.
```markdown
## TDD in Practice
Test-Driven Development (TDD) is a powerful approach that can enhance the quality of your code. Start by writing a failing test, then write the code that makes the test pass, and finally refactor the code. This approach ensures that you are always building testable functionalities.
For example, when creating a new component, begin by writing a test that describes the expected behavior. Then implement the component until the test passes. This practice not only improves code quality but also serves as living documentation of your system's behavior.
## Conclusion
Testing is a vital part of software development, and with tools like Vitest and React Testing Library, you can write effective tests that truly help ensure the quality of your code. By integrating tests into your workflow, you not only improve the stability of your application but also gain confidence in your implementations.
**Practical takeaways:**
- Use Vitest alongside Vite for a quick testing setup.
- Write unit tests with the React Testing Library to ensure your components work as expected.
- Utilize mocking to isolate functions and avoid unnecessary calls in tests.
- Conduct integration tests to verify the interaction between different parts of your system.
- Evaluate test coverage to identify areas that need more attention.
Post glossary
- react
- A JavaScript library for building user interfaces from reusable components, maintained by Meta.
- npm
- Node.js's default package manager, used to install and publish JavaScript libraries.
- dom
- Document Object Model: the browser's tree representation of a page, letting JavaScript read and modify its content.
- javascript
- The programming language that runs in browsers (and on servers via Node.js), responsible for web page interactivity.
- api
- Application Programming Interface: a set of rules that lets one program talk to another, exposing data and features in a standardized way.
- json
- JavaScript Object Notation: a lightweight key/value data-exchange format that is readable by humans and machines.
- index
- A structure that speeds up database lookups by finding records without scanning the whole table.
- html
- HyperText Markup Language: the markup language that defines the structure and content of a web page.
