October 12, 2025

Vitest is a fast, Vite-native unit testing framework that's ideal for modern JavaScript and TypeScript projects. When combined with a Vite React app, it allows seamless testing without the overhead of tools like Jest. This guide builds on a basic setup by explaining not just how to configure Vitest with import aliases, but why each step is necessary. Import aliases (e.g., @/components) improve code readability and maintainability by avoiding relative path hell, especially in larger projects. We'll cover creating the app, adding aliases, using them, and integrating Vitest for reliable testing.
Start by bootstrapping your project with Vite, which is a modern build tool focused on speed and simplicity.
Command:
pnpm create vite
Why do we need to do this?
Vite provides a lightweight, fast development server and build process optimized for React and TypeScript. Unlike Create React App, Vite uses native ES modules for instant hot module replacement (HMR) and faster cold starts. Using pnpm as the package manager ensures faster installations and disk space efficiency through a shared store for dependencies, which is crucial for reproducible builds in team environments.
Follow the prompts to select React and TypeScript templates. This sets up a basic structure with src/ as the root for your application code.
Import aliases allow you to use absolute paths like
@/components/Button../../components/ButtonAdd baseUrl and paths:
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
],
"compilerOptions": {
"baseUrl": ".",
"paths": { "@/*": ["./src/*"] },
"resolveJsonModule": true,
"esModuleInterop": true
}
}
Why?
baseUrl: "." sets the root directory for absolute imports, making paths relative to the project root. paths maps @/ to ./src/, enabling TypeScript to resolve imports correctly during compilation and IDE autocompletion. This prevents path errors in larger apps, improves refactorability (e.g., moving files without updating imports), and enhances code cleanliness. resolveJsonModule and esModuleInterop handle JSON imports and CommonJS compatibility, which are common in modern setups.
Add alias resolution:
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react-swc';
import path from 'path';
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
});
Why?
Vite needs to know how to resolve these aliases at runtime and during bundling. Without this, the dev server or build process would fail on alias imports. path.resolve ensures the alias points accurately to ./src, aligning Vite's resolver with TypeScript's for consistent behavior across development and production.
Add baseUrl and paths:
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2022",
"useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"types": ["vite/client"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"]
}
Why?
This file is specific to the app's TypeScript compilation. Duplicating baseUrl and paths here ensures the app's code (under src/) uses aliases correctly, while keeping node-specific configs separate. It enables bundler-mode resolution, which Vite relies on, and strict linting rules to catch errors early, promoting robust code.
Place components like Button.tsx under src/components/.
Example Import:
import Button from '@/components/Button';
Why?
Aliases eliminate brittle relative paths (e.g., ../../../), reducing errors when restructuring folders. This is essential for scalability— in a growing app, it saves time on maintenance and makes code more intuitive for collaborators. All files must be under src/ for the alias to resolve properly, as that's the mapped root.
Vitest integrates natively with Vite, sharing its config for faster tests.
Command:
pnpm add -D vitest @testing-library/react @testing-library/jest-dom @testing-library/user-event jsdom
Why?
Create src/demo.test.ts:
import { expect, test } from "vitest";
const sum = (a: number, b: number) => a + b;
test("adds 1 + 2 to equal 3", () => {
expect(sum(1, 2)).toBe(3);
});
Add to package.json:
"scripts": {
"test": "vitest"
}
Why?
This verifies the setup with a basic arithmetic test. Naming files with .test.ts or .test.tsx ensures Vitest auto-discovers them. The test script runs all such files, allowing continuous testing. Explaining failures (e.g., changing + to - shows expected vs. actual in green/red) helps debug why tests fail, teaching the importance of assertions for code reliability.
Run with:
pnpm run test
If it fails (e.g., due to a code mistake), Vitest highlights discrepancies, emphasizing why tests catch regressions early.
Recommended folder structure: Create tests under src/, mirroring app structure (e.g., tests/components/Button.test.tsx).
Example Test (Button.test.tsx):
import Button from '@/components/ui/button';
import { render, screen } from '@testing-library/react';
describe('Button component', () => {
describe('renders button with children', () => {
test('renders button with text', () => {
render(<Button>Simple Button</Button>);
const button = screen.getByRole('button');
expect(button).toBeInTheDocument();
expect(button).toHaveTextContent(/Simple Button/i);
});
})
});
Why?
Mirroring structure keeps tests organized and co-located with code, making maintenance easier. Using describe and test groups related assertions for readability. Testing via roles (e.g., getByRole('button')) ensures accessibility compliance. This verifies the component renders correctly, catching UI bugs before deployment.
If you see "Cannot find module '@/components/ui/button'", configure Vitest to recognize aliases.
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'jsdom',
globals: true,
setupFiles: ['./vitest.setup.ts'],
coverage: {
provider: 'v8',
},
},
resolve: {
alias: {
'@': '/src',
},
},
});
Why?
Vitest needs its own config to inherit Vite's resolver for aliases. environment: 'jsdom' simulates a browser. globals: true allows using test, expect without imports. setupFiles runs global setups. coverage with V8 tracks test coverage for quality metrics.
import '@testing-library/jest-dom';
Why?
This imports matchers globally, making them available in all tests without repetition, streamlining the testing workflow.
Add types:
"types": ["vitest/globals", "@testing-library/jest-dom"]
Why?
This provides TypeScript types for Vitest globals and Jest-DOM matchers, enabling IDE autocompletion and type safety in tests.
/// <reference types="vitest/globals" />
import '@testing-library/jest-dom';
Why?
This declaration file ensures TypeScript recognizes Vitest and testing library types project-wide, resolving type errors during compilation.
After these fixes, re-run
pnpm run testBy following this guide, you've set up a robust testing environment with Vitest in your Vite React TypeScript app. Aliases keep imports clean, and tests ensure code reliability. For visual references, refer to the original document's screenshots (e.g., image1.png for test outputs). Experiment with more complex tests to build confidence in your app!