Testing
A test is code that checks by itself that the app works correctly: shows the right text, reacts to clicks. Tests run with one command and find bugs before users see them. Vite projects use Vitest — a test runner — and Testing Library — a set of functions to render a component and find elements in it the way a person would.
Installing
In the to-do list project folder run:
npm install -D vitest jsdom @testing-library/react @testing-library/dom @testing-library/user-event @testing-library/jest-domjsdom imitates a browser inside Node.js, so components can render without a real window. The -D flag records the packages as development dependencies.
Configuring
Add a test section to vite.config.js:
// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
globals: true,
setupFiles: './src/setupTests.js',
},
});Create src/setupTests.js — it adds handy checks such as toBeInTheDocument:
// src/setupTests.js
import '@testing-library/jest-dom/vitest';And add a command to the scripts section of package.json: "test": "vitest".
A first test
A test lives next to its component and has the same name with .test.jsx. Create src/components/TodoList.test.jsx:
// src/components/TodoList.test.jsx
import { render, screen } from '@testing-library/react';
import { expect, test } from 'vitest';
import TodoList from './TodoList.jsx';
test('shows a message when there are no tasks', () => {
render(<TodoList todos={[]} />);
expect(screen.getByText('No tasks. Time to rest.')).toBeInTheDocument();
});render renders the component, screen.getByText finds an element with that text. Run npm test — the terminal shows a green mark and 1 passed. Vitest keeps watching the files and reruns the tests on save.
User actions
The user-event library clicks buttons and types text like a real user. Let us test the form — src/App.test.jsx:
// src/App.test.jsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { expect, test } from 'vitest';
import App from './App.jsx';
test('adds a task to the list', async () => {
const user = userEvent.setup();
render(<App />);
await user.type(screen.getByRole('textbox'), 'Water the plants');
await user.click(screen.getByRole('button', { name: 'Add' }));
expect(screen.getByText('Water the plants')).toBeInTheDocument();
});getByRole finds an element by its role: textbox is an input field, button is a button with the given text. The test types text, clicks "Add" and checks that the task appeared.
Ways to find elements
| Function | When to use it |
|---|---|
getByRole | Buttons, fields, links — the preferred way |
getByText | Plain text on the page |
getByLabelText | A field that has a label |
queryByText | Check that an element is absent: returns null |
findByText | Wait for an element that appears later |
Common mistake
user-event actions are asynchronous. Without await the check runs before typing finishes, and the test fails:
// Wrong:
user.type(screen.getByRole('textbox'), 'Water the plants');
// Correct:
await user.type(screen.getByRole('textbox'), 'Water the plants');Mark the test function as async and put await before every user action.