Skip to content

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-event

Wire 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 div has 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,

-- ł.