8.9 KiB
8.9 KiB
Testing Setup Reference
Complete configuration guide for Vitest, React Testing Library, and MSW in TanStack projects.
Vitest Configuration
vitest.config.ts
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
import path from 'path';
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['./src/test/setup.ts'],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: [
'node_modules/',
'src/test/',
'**/*.d.ts',
'**/*.config.*',
'**/mockData',
],
thresholds: {
lines: 80,
functions: 80,
branches: 80,
statements: 80,
},
},
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
});
Key Options
| Option | Purpose | Value |
|---|---|---|
globals |
Enable global test APIs (describe, it, expect) | true |
environment |
Test environment (jsdom for DOM testing) | 'jsdom' |
setupFiles |
Files to run before each test file | ['./src/test/setup.ts'] |
coverage.provider |
Coverage provider | 'v8' (faster) |
coverage.thresholds |
Minimum coverage percentages | 80% recommended |
Test Setup File
src/test/setup.ts
import '@testing-library/jest-dom/vitest';
import { cleanup } from '@testing-library/react';
import { afterEach, beforeAll, afterAll, vi } from 'vitest';
import { server } from './msw/server';
// Cleanup after each test
afterEach(() => {
cleanup();
});
// MSW setup
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
// Mock window.matchMedia (for responsive components)
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: vi.fn().mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
});
// Mock IntersectionObserver (for infinite scroll)
global.IntersectionObserver = vi.fn().mockImplementation(() => ({
observe: vi.fn(),
unobserve: vi.fn(),
disconnect: vi.fn(),
}));
// Mock ResizeObserver (for table columns)
global.ResizeObserver = vi.fn().mockImplementation(() => ({
observe: vi.fn(),
unobserve: vi.fn(),
disconnect: vi.fn(),
}));
// Suppress console errors in tests (optional)
const originalError = console.error;
beforeAll(() => {
console.error = (...args: any[]) => {
if (
typeof args[0] === 'string' &&
args[0].includes('Warning: ReactDOM.render')
) {
return;
}
originalError.call(console, ...args);
};
});
afterAll(() => {
console.error = originalError;
});
MSW Setup
src/test/msw/server.ts
import { setupServer } from 'msw/node';
import { handlers } from './handlers';
export const server = setupServer(...handlers);
src/test/msw/handlers.ts
import { http, HttpResponse } from 'msw';
export const handlers = [
// Users API
http.get('/api/users', () => {
return HttpResponse.json([
{ id: '1', name: 'Alice', email: 'alice@example.com' },
{ id: '2', name: 'Bob', email: 'bob@example.com' },
]);
}),
http.get('/api/users/:id', ({ params }) => {
return HttpResponse.json({
id: params.id,
name: 'Alice',
email: 'alice@example.com',
});
}),
http.post('/api/users', async ({ request }) => {
const body = await request.json();
return HttpResponse.json(
{ id: '3', ...body },
{ status: 201 }
);
}),
// Auth API
http.post('/api/auth/login', async ({ request }) => {
const { email, password } = await request.json();
if (email === 'test@example.com' && password === 'password') {
return HttpResponse.json({
token: 'mock-jwt-token',
user: { id: '1', email, name: 'Test User' },
});
}
return HttpResponse.json(
{ error: 'Invalid credentials' },
{ status: 401 }
);
}),
];
Overriding Handlers in Tests
import { server } from '../test/msw/server';
import { http, HttpResponse } from 'msw';
it('handles API error', async () => {
// Override handler for this test
server.use(
http.get('/api/users', () => {
return new HttpResponse(null, { status: 500 });
})
);
// Test error handling...
});
TanStack Query Setup
src/test/query-client.ts
import { QueryClient } from '@tanstack/react-query';
export function createTestQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
retry: false, // Don't retry failed queries in tests
gcTime: 0, // No garbage collection
staleTime: 0, // Always consider data stale
},
mutations: {
retry: false,
},
},
logger: {
log: console.log,
warn: console.warn,
error: () => {}, // Silence error logs in tests
},
});
}
TanStack Router Setup
src/test/router-utils.tsx
import { createMemoryHistory, createRouter } from '@tanstack/react-router';
import { routeTree } from '../routeTree.gen';
export function createTestRouter(initialEntries = ['/']) {
const history = createMemoryHistory({ initialEntries });
return createRouter({
routeTree,
history,
context: {
// Mock auth context
auth: {
isAuthenticated: true,
user: { id: '1', name: 'Test User' },
},
},
});
}
Custom Test Utilities
src/test/test-utils.tsx
import { ReactElement } from 'react';
import { render, RenderOptions } from '@testing-library/react';
import { QueryClientProvider } from '@tanstack/react-query';
import { RouterProvider } from '@tanstack/react-router';
import { createTestQueryClient } from './query-client';
import { createTestRouter } from './router-utils';
interface WrapperProps {
children: React.ReactNode;
}
export function AllTheProviders({ children }: WrapperProps) {
const queryClient = createTestQueryClient();
const router = createTestRouter();
return (
<QueryClientProvider client={queryClient}>
<RouterProvider router={router}>
{children}
</RouterProvider>
</QueryClientProvider>
);
}
export function renderWithProviders(
ui: ReactElement,
options?: Omit<RenderOptions, 'wrapper'>
) {
return render(ui, { wrapper: AllTheProviders, ...options });
}
// Re-export everything from React Testing Library
export * from '@testing-library/react';
export { renderWithProviders as render };
Package.json Scripts
{
"scripts": {
"test": "vitest",
"test:ui": "vitest --ui",
"test:coverage": "vitest --coverage",
"test:run": "vitest run"
},
"devDependencies": {
"@testing-library/jest-dom": "^6.1.5",
"@testing-library/react": "^14.1.2",
"@testing-library/user-event": "^14.5.1",
"@vitest/coverage-v8": "^1.0.4",
"@vitest/ui": "^1.0.4",
"jsdom": "^23.0.1",
"msw": "^2.0.11",
"vitest": "^1.0.4"
}
}
TypeScript Configuration
tsconfig.json
{
"compilerOptions": {
"types": ["vitest/globals", "@testing-library/jest-dom"]
}
}
Coverage Configuration
.gitignore
coverage/
.vitest/
Coverage Thresholds
// vitest.config.ts
export default defineConfig({
test: {
coverage: {
thresholds: {
lines: 80,
functions: 80,
branches: 80,
statements: 80,
// Per-file thresholds
perFile: true,
},
},
},
});
Environment Variables
.env.test
VITE_API_URL=http://localhost:3000/api
VITE_ENV=test
Loading in Tests
// src/test/setup.ts
import { loadEnv } from 'vite';
const env = loadEnv('test', process.cwd(), '');
process.env = { ...process.env, ...env };
CI/CD Integration
GitHub Actions
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '20'
- run: npm ci
- run: npm run test:coverage
- uses: codecov/codecov-action@v3
with:
files: ./coverage/coverage-final.json
Troubleshooting
Common Issues
Issue: ReferenceError: describe is not defined
Solution: Add globals: true to vitest config
Issue: Cannot find module '@testing-library/jest-dom/vitest'
Solution: Install @testing-library/jest-dom package
Issue: MSW not intercepting requests
Solution: Ensure server.listen() is called in beforeAll
Issue: Tests fail with "Act" warnings
Solution: Wrap async operations with waitFor or findBy
Next: Testing Best Practices | Index: Reference Index