This README explains how to set up unit testing for a React 19+ app using Vite + Vitest + Testing Library. It includes packages to install, configuration, example tests (user-event), coverage configuration, Test UI, useful scripts, and troubleshooting.
- Node.js (v16+ recommended)
- npm (or yarn/pnpm)
- Vite project created (React + Vite)
Run this in your project root:
npm install --save-dev vitest @vitest/ui @vitest/coverage-v8 jsdom @testing-library/react @testing-library/jest-dom @testing-library/user-event
What each does:
vitest- test runner@vitest/ui- optional web UI for Vitest@vitest/coverage-v8- V8-based coverage provider (fast)jsdom- DOM environment for tests@testing-library/react- render + query helpers@testing-library/jest-dom- extraexpectmatchers@testing-library/user-event- higher-level, realistic user interactions
Add these to your package.json scripts section:
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview",
"test": "vitest",
"test:ui": "vitest --ui",
"test:watch": "vitest --watch",
"test:coverage": "vitest run --coverage"
}npm run test— run tests in terminal (interactive)npm run test:ui— open web UI (interactive browser)npm run test:watch— watch modenpm run test:coverage— run tests and generate coverage reports
Create vitest.config.js (or vite.config.js if you prefer) in project root. Example using vitest/config:
// vitest.config.js
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
test: {
globals: true, // use global test APIs (describe/it/expect)
environment: 'jsdom', // simulate browser DOM
setupFiles: './src/setupTests.js',
coverage: {
provider: 'v8', // 'v8' via @vitest/coverage-v8
reportsDirectory: './coverage',
reporter: ['text', 'html'],
all: true,
include: ['src/**/*.{js,jsx,ts,tsx}']
}
}
})If you already have
vite.config.js, you can add atestfield to it instead of creating a separate file.
Create src/setupTests.js and add:
import '@testing-library/jest-dom'This registers extra matchers like toBeInTheDocument().
src/
components/
TodoForm.jsx
__tests__/
TodoForm.test.jsx
context/
TodoContext.jsx
setupTests.js
vite.config.js (or vitest.config.js)
package.json
// src/components/__tests__/TodoForm.test.jsx
import { describe, it, expect, vi } from 'vitest'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import TodoForm from '../TodoForm'
import { TodoContext } from '../../context/TodoContext'
describe('TodoForm Component (userEvent)', () => {
it('should submit the form and call addTodo', async () => {
const addTodo = vi.fn()
const user = userEvent.setup()
render(
<TodoContext.Provider value={{ addTodo }}>
<TodoForm />
</TodoContext.Provider>
)
const titleInput = screen.getByPlaceholderText(/Enter title/i)
const statusSelect = screen.getByRole('combobox')
const submitButton = screen.getByRole('button', { name: /add/i })
await user.type(titleInput, 'Test Todo')
await user.selectOptions(statusSelect, 'Pending')
await user.click(submitButton)
expect(addTodo).toHaveBeenCalledTimes(1)
const arg = addTodo.mock.calls[0][0]
expect(arg).toEqual(expect.objectContaining({ title: 'Test Todo', status: 'Pending' }))
expect(typeof arg.id).toBe('number')
expect(titleInput.value).toBe('')
expect(statusSelect.value).toBe('Todo')
})
})it('should not call addTodo when title is empty', async () => {
const addTodo = vi.fn()
const user = userEvent.setup()
render(
<TodoContext.Provider value={{ addTodo }}>
<TodoForm />
</TodoContext.Provider>
)
const submitButton = screen.getByRole('button', { name: /add/i })
await user.click(submitButton)
expect(addTodo).not.toHaveBeenCalled()
})- Run tests (terminal):
npm run test- Run tests in watch mode:
npm run test:watch- Open Vitest UI:
npm run test:ui- Run tests + coverage and generate reports:
npm run test:coverageAfter coverage run, open coverage/index.html in your browser (open the file directly or serve the folder).
ReferenceError: window is not defined— ensureenvironment: 'jsdom'in config.No coverage provider configured— make sure@vitest/coverage-v8is installed andcoverage.providerset tov8(or useistanbul).- Tests failing due to missing setup — ensure
src/setupTests.jsis configured andsetupFilespoints to it. - Tests timing out or flaky — prefer
userEvent.setup()andawaiton slow interactions.
- Prefer
user-eventoverfireEventfor realistic interactions. - Use AAA (Arrange, Act, Assert) structure in tests.
- Mock external network calls (fetch/axios) using
vi.stubGlobalorvi.mock. - Keep tests focused and deterministic.
{
"name": "my-app",
"version": "1.0.0",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"test": "vitest",
"test:ui": "vitest --ui",
"test:coverage": "vitest run --coverage"
},
"devDependencies": {
"vitest": "^",
"@vitest/ui": "^",
"@vitest/coverage-v8": "^",
"jsdom": "^",
"@testing-library/react": "^",
"@testing-library/jest-dom": "^",
"@testing-library/user-event": "^"
}
}