The boilerplate already includes testing dependencies:
{
"devDependencies": {
"vitest": "^1.0.0",
"@testing-library/react": "^14.0.0",
"@testing-library/jest-dom": "^6.0.0",
"@testing-library/user-event": "^14.0.0",
"jsdom": "^23.0.0"
}
}// vite.config.js
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
globals: true,
setupFiles: './tests/setup.js',
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: [
'node_modules/',
'tests/',
'**/*.config.js',
],
},
},
});// tests/setup.js
import '@testing-library/jest-dom';
import { cleanup } from '@testing-library/react';
import { afterEach, vi } from 'vitest';
afterEach(() => {
cleanup();
});
global.fetch = vi.fn();npm run test # Run tests once
npm run test:watch # Run tests in watch mode
npm run test:ui # Run tests with UI
npm run test:coverage # Run tests with coverage| Type | Target |
|---|---|
| Statements | 80% |
| Branches | 80% |
| Functions | 80% |
| Lines | 80% |
tests/
├── setup.js # Test configuration
├── fixtures/ # Mock data
│ └── users.json
├── components/ # Component tests
│ ├── Button.test.jsx
│ └── Input.test.jsx
├── features/ # Feature tests
│ ├── auth/
│ │ └── authSlice.test.js
│ └── products/
│ └── productSlice.test.js
└── utils/ # Utility tests
└── validators.test.js
// tests/components/Button.test.jsx
import { render, screen } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import { Button } from '../../src/components/ui/Button';
describe('Button', () => {
it('renders with text', () => {
render(<Button>Click me</Button>);
expect(screen.getByRole('button')).toHaveTextContent('Click me');
});
it('renders with variant', () => {
render(<Button variant="primary">Submit</Button>);
const button = screen.getByRole('button');
expect(button).toHaveClass('bg-blue-500');
});
it('handles click events', () => {
const handleClick = vi.fn();
render(<Button onClick={handleClick}>Click me</Button>);
screen.getByRole('button').click();
expect(handleClick).toHaveBeenCalledTimes(1);
});
it('is disabled when disabled prop is true', () => {
render(<Button disabled>Disabled</Button>);
expect(screen.getByRole('button')).toBeDisabled();
});
});// tests/features/auth/LoginForm.test.jsx
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Provider } from 'react-redux';
import { configureStore } from '@reduxjs/toolkit';
import { BrowserRouter } from 'react-router-dom';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { LoginForm } from '../../../src/features/auth/components/LoginForm';
const mockLogin = vi.fn();
vi.mock('../../../src/features/auth/hooks/useAuth', () => ({
useAuth: () => ({
login: mockLogin,
isLoading: false,
}),
}));
const renderWithProviders = (component) => {
return render(
<BrowserRouter>
{component}
</BrowserRouter>
);
};
describe('LoginForm', () => {
beforeEach(() => {
mockLogin.mockClear();
});
it('renders login form', () => {
renderWithProviders(<LoginForm />);
expect(screen.getByLabelText(/email/i)).toBeInTheDocument();
expect(screen.getByLabelText(/password/i)).toBeInTheDocument();
expect(screen.getByRole('button', { name: /sign in/i })).toBeInTheDocument();
});
it('shows validation errors for empty fields', async () => {
renderWithProviders(<LoginForm />);
await userEvent.click(screen.getByRole('button', { name: /sign in/i }));
await waitFor(() => {
expect(screen.getByText(/email is required/i)).toBeInTheDocument();
expect(screen.getByText(/password is required/i)).toBeInTheDocument();
});
});
it('calls login with form data on submit', async () => {
const testCredentials = {
email: 'test@example.com',
password: 'password123',
};
mockLogin.mockResolvedValue({ success: true });
renderWithProviders(<LoginForm />);
await userEvent.type(screen.getByLabelText(/email/i), testCredentials.email);
await userEvent.type(screen.getByLabelText(/password/i), testCredentials.password);
await userEvent.click(screen.getByRole('button', { name: /sign in/i }));
await waitFor(() => {
expect(mockLogin).toHaveBeenCalledWith(testCredentials);
});
});
});// tests/features/auth/authSlice.test.js
import { describe, it, expect } from 'vitest';
import authReducer, {
login,
logout,
setUser,
} from '../../../src/features/auth/slices/authSlice';
describe('authSlice', () => {
const initialState = {
user: null,
token: null,
loading: false,
error: null,
};
it('should return initial state', () => {
expect(authReducer(undefined, { type: 'unknown' })).toEqual(initialState);
});
it('should handle login.pending', () => {
const actual = authReducer(initialState, login.pending());
expect(actual.loading).toBe(true);
expect(actual.error).toBeNull();
});
it('should handle login.fulfilled', () => {
const user = { id: 1, email: 'test@example.com' };
const token = 'jwt-token';
const actual = authReducer(
{ ...initialState, loading: true },
login.fulfilled({ user, token })
);
expect(actual.loading).toBe(false);
expect(actual.user).toEqual(user);
expect(actual.token).toBe(token);
});
it('should handle login.rejected', () => {
const error = 'Invalid credentials';
const actual = authReducer(
{ ...initialState, loading: true },
login.rejected(null, null, null, error)
);
expect(actual.loading).toBe(false);
expect(actual.error).toBe(error);
});
it('should handle logout', () => {
const stateWithUser = {
...initialState,
user: { id: 1, email: 'test@example.com' },
token: 'jwt-token',
};
const actual = authReducer(stateWithUser, logout());
expect(actual.user).toBeNull();
expect(actual.token).toBeNull();
});
it('should handle setUser', () => {
const user = { id: 1, name: 'John' };
const actual = authReducer(initialState, setUser(user));
expect(actual.user).toEqual(user);
});
});// tests/utils/validators.test.js
import { describe, it, expect } from 'vitest';
import {
validateEmail,
validatePassword,
validateRequired,
} from '../../src/utils/validators';
describe('validators', () => {
describe('validateEmail', () => {
it('returns true for valid email', () => {
expect(validateEmail('test@example.com')).toBe(true);
});
it('returns false for invalid email', () => {
expect(validateEmail('invalid')).toBe(false);
expect(validateEmail('test@')).toBe(false);
expect(validateEmail('@example.com')).toBe(false);
});
});
describe('validatePassword', () => {
it('returns true for valid password', () => {
expect(validatePassword('password123')).toBe(true);
expect(validatePassword('myP@ssw0rd')).toBe(true);
});
it('returns false for short password', () => {
expect(validatePassword('short')).toBe(false);
});
it('returns false for password without number', () => {
expect(validatePassword('password')).toBe(false);
});
});
describe('validateRequired', () => {
it('returns true for non-empty value', () => {
expect(validateRequired('hello')).toBe(true);
expect(validateRequired(123)).toBe(true);
});
it('returns false for empty value', () => {
expect(validateRequired('')).toBe(false);
expect(validateRequired(null)).toBe(false);
expect(validateRequired(undefined)).toBe(false);
});
});
});// tests/fixtures/users.json
[
{
"id": 1,
"name": "John Doe",
"email": "john@example.com",
"role": "admin"
},
{
"id": 2,
"name": "Jane Smith",
"email": "jane@example.com",
"role": "user"
}
]// tests/mocks/handlers.js
import { http, HttpResponse } from 'msw';
export const handlers = [
http.get('/api/users', () => {
return HttpResponse.json([
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' },
]);
}),
http.post('/api/login', async ({ request }) => {
const { email, password } = await request.json();
if (email === 'test@example.com' && password === 'password123') {
return HttpResponse.json({
user: { id: 1, email },
token: 'jwt-token',
});
}
return HttpResponse.json(
{ message: 'Invalid credentials' },
{ status: 401 }
);
}),
];| Do | Don't |
|---|---|
| Test behavior, not implementation | Test implementation details |
| Use meaningful test names | Use vague names like "test1" |
| Follow AAA pattern (Arrange, Act, Assert) | Mix multiple tests |
| Test happy path and edge cases | Only test happy path |
| Keep tests independent | Rely on test order |
| Mock external dependencies | Make real API calls |
Run coverage to see what's tested:
npm run test:coverageView detailed report in coverage/index.html.