If you use Vite + React + TypeScript, the fastest way to add tests is Vitest.
In this post, I will show a clean setup you can copy.
Why this stack?
- Vite: fast dev server
- Vitest: test runner that feels like Jest, but faster in Vite projects
- React Testing Library: test from user perspective
This setup works well with pnpm and small-to-medium frontend apps.
1) Install packages
pnpm add -D vitest jsdom @testing-library/react @testing-library/jest-dom @testing-library/user-event
2) Update vite.config.ts
/// <reference types="vitest" />
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
setupFiles: './src/test/setup.ts',
globals: true,
},
})
What this does:
-
jsdomgives a browser-like environment -
setupFilesruns once before tests -
globals: truelets you usedescribe,it,expectwithout importing every time
3) Create src/test/setup.ts
import '@testing-library/jest-dom'
This adds helpers like toBeInTheDocument().
4) Add scripts to package.json
{
"scripts": {
"test": "vitest run",
"test:watch": "vitest",
"test:ui": "vitest --ui"
}
}
Use:
-
pnpm testfor CI -
pnpm test:watchwhile coding
5) Example component test
src/components/Counter.tsx
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>
)
}
src/components/Counter.test.tsx
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { Counter } from './Counter'
describe('Counter', () => {
it('increments count when button is clicked', async () => {
render(<Counter />)
await userEvent.click(screen.getByRole('button', { name: /increment/i }))
expect(screen.getByText('Count: 1')).toBeInTheDocument()
})
})
This test checks real behavior, not internal state details.
Common beginner mistakes
-
Using
nodeenvironment for React tests (usejsdom) - Forgetting
@testing-library/jest-domsetup - Testing implementation details instead of visible UI behavior
Final tip
Start with one test per component for the core user action.
You do not need 100 tests on day one.
Small, stable tests are better than many fragile tests.
If you want, next step is adding coverage and running Vitest in CI.
Top comments (1)
Clean setup — almost identical to what I landed on for a finance planning tool built with Vite + React + TS.
One thing that saved me a lot of grief: adding
css.modules.localsConventionto the Vitest config so CSS Module classnames resolve the same way in tests as in dev. Without it, component snapshots break silently when you use camelCase imports but your.module.cssuses kebab-case. Small thing, but it cost me an afternoon the first time.The "one test per component for core user action" advice is spot on. I started with that philosophy and now have 900+ tests — but the first 50 were the ones that actually caught real bugs. The rest mostly catch regressions during refactors, which is valuable but different.
Curious about one thing: do you run
vitest runin CI with--reporter=verboseor stick with the default? I found that verbose mode in GitHub Actions logs makes it much easier to debug flaky tests remotely, but it does make the output noisy for large suites. Any sweet spot you have found?Also the "test behavior not implementation" point deserves emphasis — I had tests checking internal useState values early on, and every small refactor broke them. Switching to
screen.getByText/getByRolemade the suite 10x more stable.