Let's configure a modern, professional testing environment for React — the same stack used by industry teams.
🎯 What We're Building
This setup combines:
- Vite → Lightning-fast dev server and build tool
- Vitest → Modern, Jest-compatible test runner with native ESM support
- React Testing Library → User-centric UI testing that mimics real interactions
- @testing-library/jest-dom → Readable, semantic assertions
- jsdom → Lightweight browser environment for Node.js
🪜 Step 1: Install Dependencies
Run this in your project root:
npm install -D vitest @testing-library/react @testing-library/user-event @testing-library/jest-dom jsdom
What -D means: These are devDependencies — they're only needed during development, not in production.
🪜 Step 2: Configure Vitest
Create or update vite.config.ts in your project root:
/// <reference types="vitest" />
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
test: {
globals: true, // Enables 'test', 'expect' globally (no imports needed)
environment: "jsdom", // Simulates browser DOM for React components
setupFiles: "./src/setupTests.ts", // Runs before each test file
},
});
Why this matters:
-
globals: true→ Write tests without repetitive imports -
environment: "jsdom"→ Your components render as if in a real browser -
setupFiles→ Configure test helpers once, use everywhere
🪜 Step 3: Create Test Setup File
Create src/setupTests.ts:
import "@testing-library/jest-dom";
What this unlocks:
Now you can use human-readable assertions like:
expect(element).toBeInTheDocument();
expect(button).toBeDisabled();
expect(link).toHaveAttribute("href", "/home");
expect(heading).toHaveTextContent("Welcome");
Instead of verbose, fragile alternatives like:
// ❌ Without jest-dom
expect(element).toBeTruthy();
expect(button.disabled).toBe(true);
🪜 Step 4: Configure TypeScript
Update tsconfig.json to recognize Vitest and the DOM environment:
{
"compilerOptions": {
"types": ["vitest/globals", "vite/client", "@testing-library/jest-dom"],
"jsx": "react-jsx",
"module": "ESNext",
"target": "ES2022",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src"]
}
Key additions:
-
"vitest/globals"→ TypeScript recognizestest,expect,describe -
"@testing-library/jest-dom"→ Autocomplete for custom matchers
🪜 Step 5: Configure ESLint for Testing
Install the ESLint plugins for React Testing Library and jest-dom:
npm install -D eslint-plugin-testing-library eslint-plugin-jest-dom
Update your eslint.config.js (ESLint flat config):
import js from "@eslint/js";
import globals from "globals";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import tseslint from "typescript-eslint";
import { defineConfig, globalIgnores } from "eslint/config";
import eslintConfigPrettier from "eslint-config-prettier";
import testingLibrary from "eslint-plugin-testing-library";
import jestDom from "eslint-plugin-jest-dom";
export default defineConfig([
globalIgnores(["dist"]),
{
files: ["**/*.{ts,tsx}"],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs["recommended-latest"],
reactRefresh.configs.vite,
eslintConfigPrettier,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
},
// Testing Library + Jest DOM rules for test files only
{
files: [
"**/__tests__/**/*.{js,jsx,ts,tsx}",
"**/*.{test,spec}.{js,jsx,ts,tsx}",
],
extends: [
testingLibrary.configs["flat/react"],
jestDom.configs["flat/recommended"],
],
},
]);
What this provides:
✅ Best practice enforcement → Warns against anti-patterns like getByTestId
✅ Async handling → Catches missing await on async queries
✅ Accessible queries → Suggests using getByRole over less semantic queries
✅ jest-dom matchers → Autocomplete and validation for custom assertions
Example linting feedback:
// ❌ ESLint will warn
const button = container.querySelector('button');
// ✅ ESLint suggests
const button = screen.getByRole('button', { name: /submit/i });
🪜 Step 6: Add Test Script
In package.json:
"scripts": {
"dev": "vite",
"build": "vite build",
"test": "vitest",
"test:ui": "vitest --ui" // optional UI runner
}
Run tests with:
npm run test
or start an interactive UI:
npm run test:ui
🧠 Why This Setup Works
✅ Vitest understands modern ES Modules (no CommonJS issues)
✅ React Testing Library gives realistic user-focused queries
✅ JSDOM simulates the browser for rendering
✅ jest-dom improves readability of test results
✅ setupTests.ts ensures global test behavior is consistent
✅ Example: A Complete Test
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import "@testing-library/jest-dom";
import App from "./App";
test("user can add a task", async () => {
render(<App />);
const input = screen.getByRole("textbox", { name: /add task/i });
const button = screen.getByRole("button", { name: /add/i });
await userEvent.type(input, "Learn TDD");
await userEvent.click(button);
expect(screen.getByText("Learn TDD")).toBeInTheDocument();
});
When you run it, Vitest uses:
- JSDOM to render your app
- Testing Library to simulate user behavior
- jest-dom to verify output
💡 Pro Tip
You can watch tests live as you code:
npx vitest --watch
Vitest will re-run only the tests affected by the files you changed — perfect for iterative TDD development.
🏁 Final Thoughts
With this setup, you now have a professional-grade testing environment that mirrors what modern React teams use in production:
- ⚡ Vite → fast bundling
- 🧪 Vitest → Jest-compatible testing
- 🧍♂️ React Testing Library → test like a real user
- 🧱 TypeScript → safe and typed components
- 🧭 TDD workflow → focused, confident coding
✅ "Write tests for behavior, not for implementation."
🎓 Remember
"The more your tests resemble the way your software is used, the more confidence they can give you."
— Kent C. Dodds, creator of React Testing Library
With this setup, you're not just testing code — you're verifying that real users can accomplish their goals.
Next steps: Start practicing TDD by writing a failing test first, then implementing just enough code to make it pass. 🎯
Top comments (0)