In this mini-guide, you will:
Create / update config & scripts
package.json → add test scriptsvite.config.js → add test config for VitestAdd a global test setup file
setupTests.js → configure React Testing Library + jest-domCreate components to test
src/components/Hello.jsx → simple componentsrc/components/Counter.jsx → component with state + buttonCreate test files
src/components/Hello.test.jsx → tests for Hellosrc/components/Counter.test.jsx → tests for CounterAt the end, you’ll have Vitest running and two working React tests.
Here’s what your project structure will look like after following all the steps (only key files shown):
my-react-app/
├─ package.json # ← add test scripts
├─ vite.config.js # ← add Vitest config
├─ setupTests.js # ← new: jest-dom + cleanup
├─ index.html
├─ src/
│ ├─ main.jsx
│ ├─ App.jsx
│ ├─ components/
│ │ ├─ Hello.jsx # ← new component
│ │ ├─ Hello.test.jsx # ← new test file
│ │ ├─ Counter.jsx # ← new component
│ │ └─ Counter.test.jsx # ← new test file
│ └─ ...
└─ ...
If you already have a Vite React app, skip to step 2.
npm create vite@latest my-react-app -- --template react
cd my-react-app
npm install
Run this inside your project:
npm install -D vitest jsdom \
@testing-library/react \
@testing-library/jest-dom \
@testing-library/user-event
What these do:
vitest → the test runnerjsdom → fake browser environment for tests@testing-library/react → render React components in tests@testing-library/jest-dom → nicer assertions (toBeInTheDocument, etc.)@testing-library/user-event → simulate real user actions (click, type…)File we touch: setupTests.js (new)
Create a new file in the project root called setupTests.js:
// setupTests.js
import { afterEach } from 'vitest'
import { cleanup } from '@testing-library/react'
// Adds jest-dom matchers like toBeInTheDocument, toHaveTextContent, etc.
import '@testing-library/jest-dom/vitest'
// Automatically unmount and cleanup DOM after each test
afterEach(() => {
cleanup()
})
This file:
vite.config.jsFile we touch: vite.config.js
Open vite.config.js and update it to look something like this:
// vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { configDefaults } from 'vitest/config'
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom', // use jsdom to simulate a browser
globals: true, // so we can use describe/it/expect globally
setupFiles: './setupTests.js', // run our setup before tests
exclude: [...configDefaults.exclude, 'e2e/**'], // optional
},
})
The important parts:
environment: 'jsdom' → needed for DOM + React testssetupFiles → tells Vitest to load setupTests.js firstpackage.jsonFile we touch: package.json
Open package.json and add these scripts inside "scripts":
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"test": "vitest",
"test:run": "vitest run"
}
Now you can run:
npm test → watch modenpm run test:run → run tests onceHelloFile we touch: src/components/Hello.jsx (new)
// src/components/Hello.jsx
export function Hello({ name }) {
return <h1>Hello, {name}!</h1>
}
Hello componentFile we touch: src/components/Hello.test.jsx (new)
// src/components/Hello.test.jsx
import { render, screen } from '@testing-library/react'
import { Hello } from './Hello'
describe('Hello component', () => {
it('shows a greeting with the name', () => {
render(<Hello name="Student" />)
// Find the element by its text content
const heading = screen.getByText('Hello, Student!')
// This matcher comes from jest-dom via setupTests.js
expect(heading).toBeInTheDocument()
})
})
What’s happening:
render(<Hello />) → mounts the component into a virtual DOMscreen.getByText → finds what the user would visually seetoBeInTheDocument() → checks that it exists in the rendered outputCounterFile we touch: src/components/Counter.jsx (new)
// src/components/Counter.jsx
import { useState } from 'react'
export function Counter() {
const [count, setCount] = useState(0)
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
</div>
)
}
Counter componentFile we touch: src/components/Counter.test.jsx (new)
// src/components/Counter.test.jsx
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { Counter } from './Counter'
describe('Counter', () => {
it('increments when the button is clicked', async () => {
render(<Counter />)
// Find the button by its role and accessible name
const button = screen.getByRole('button', { name: /increment/i })
// Simulate a user click
await userEvent.click(button)
// After clicking, the text should update
expect(screen.getByText('Count: 1')).toBeInTheDocument()
})
})
Key ideas:
userEvent.click simulates a real user clickCount: 1)Now run:
npm test
You should see Vitest start, discover *.test.jsx files, and run them.
If everything’s correct, both Hello and Counter tests should pass ✅
Dependencies installed
vitest, jsdom, @testing-library/react, @testing-library/jest-dom, @testing-library/user-eventsetupTests.js created
@testing-library/jest-dom/vitestcleanup() in afterEachvite.config.js updated
test.environment = 'jsdom'test.globals = truetest.setupFiles = './setupTests.js'package.json updated
"test": "vitest""test:run": "vitest run"Components created
src/components/Hello.jsxsrc/components/Counter.jsxTests created
src/components/Hello.test.jsxsrc/components/Counter.test.jsxnpm test runs successfully and passes all tests
For more advanced testing patterns and additional query methods, check out the official React Testing Library documentation:
toBeInTheDocument, toHaveClass, etc.