Appearance
Testing React without turning it into a second job
I don't test every component. I test the stuff that breaks silently and the flows users actually click.
Tooling
Common Vite SPA setup:
bash
npm install -D vitest jsdom @testing-library/react @testing-library/jest-dom @testing-library/user-eventWire Vitest to Vite in vite.config.ts (simplified):
ts
/// <reference types="vitest/config" />
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
globals: true,
setupFiles: './src/test/setup.ts',
},
})ts
// src/test/setup.ts
import '@testing-library/jest-dom'Component smoke test
tsx
// Counter.tsx
import { useState } from 'react'
export function Counter() {
const [count, setCount] = useState(0)
return (
<button type="button" onClick={() => setCount((c) => c + 1)}>
count is {count}
</button>
)
}tsx
// Counter.test.tsx
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { Counter } from './Counter'
test('increments on click', async () => {
const user = userEvent.setup()
render(<Counter />)
const button = screen.getByRole('button', { name: /count is 0/i })
await user.click(button)
expect(screen.getByRole('button', { name: /count is 1/i })).toBeInTheDocument()
})Comments for the code above:
- Query by role / text - closer to how users see the UI (Vue Testing Library is the same idea)
userEvent> firing raw click events- Assert on the outcome, not on internal state
What I actually test
Worth it:
- Form validation messages
- Conditional UI (auth gates, empty states)
- Tricky reducers / pure helpers (easy unit tests)
- A couple of critical user flows
Usually not worth it early:
- Snapshotting entire pages
- Testing that a
divhas a class name - Re-implementing the browser
Async / Query note
If you test components wrapped in TanStack Query, wrap them in a QueryClientProvider with a fresh QueryClient per test. Same idea as mounting with plugins in Vue Test Utils.
As you can see - the mindset transfers. Different imports, same "act like a user" energy.
That closes the SPA path. Next section: Next.js - Nuxt's cousin with Server Components.
Best,
-- ł.