Initial commit
This commit is contained in:
421
skills/react-tanstack-testing/reference/testing-setup.md
Normal file
421
skills/react-tanstack-testing/reference/testing-setup.md
Normal file
@@ -0,0 +1,421 @@
|
||||
# Testing Setup Reference
|
||||
|
||||
Complete configuration guide for Vitest, React Testing Library, and MSW in TanStack projects.
|
||||
|
||||
## Vitest Configuration
|
||||
|
||||
### vitest.config.ts
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
import { setupServer } from 'msw/node';
|
||||
import { handlers } from './handlers';
|
||||
|
||||
export const server = setupServer(...handlers);
|
||||
```
|
||||
|
||||
### src/test/msw/handlers.ts
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```json
|
||||
{
|
||||
"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
|
||||
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"types": ["vitest/globals", "@testing-library/jest-dom"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Coverage Configuration
|
||||
|
||||
### .gitignore
|
||||
|
||||
```
|
||||
coverage/
|
||||
.vitest/
|
||||
```
|
||||
|
||||
### Coverage Thresholds
|
||||
|
||||
```typescript
|
||||
// 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
|
||||
|
||||
```bash
|
||||
VITE_API_URL=http://localhost:3000/api
|
||||
VITE_ENV=test
|
||||
```
|
||||
|
||||
### Loading in Tests
|
||||
|
||||
```typescript
|
||||
// src/test/setup.ts
|
||||
import { loadEnv } from 'vite';
|
||||
|
||||
const env = loadEnv('test', process.cwd(), '');
|
||||
process.env = { ...process.env, ...env };
|
||||
```
|
||||
|
||||
## CI/CD Integration
|
||||
|
||||
### GitHub Actions
|
||||
|
||||
```yaml
|
||||
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](testing-best-practices.md) | **Index**: [Reference Index](INDEX.md)
|
||||
Reference in New Issue
Block a user