feat: improved the Claude Kit as a plugin

This commit is contained in:
duthaho
2026-04-19 14:09:14 +07:00
parent 3103a8da1b
commit d1a6d2a2bc
186 changed files with 771 additions and 1691 deletions
+64
View File
@@ -0,0 +1,64 @@
---
name: testing
description: >
Use when writing, debugging, or configuring unit or integration tests with pytest, Vitest, or Jest. Also activate for fixtures, mocking, coverage, parametrization, jest.mock, vi.mock, jest.fn, vi.fn, conftest.py, vitest.config.ts, jest.config, Testing Library, @jest/globals, or any test configuration.
---
# Testing
## When to Use
- Writing Python tests with pytest (fixtures, parametrize, markers, coverage)
- Testing JavaScript/TypeScript with Vitest (React components, mocking, workspace)
- NestJS or existing projects using Jest
- Debugging test configuration, ESM issues, or flaky tests
- Setting up coverage, CI integration, or test infrastructure
## When NOT to Use
- E2E browser testing — use `playwright`
- Testing anti-patterns and methodology — use `testing-anti-patterns`
- TDD workflow — use `test-driven-development`
---
## Quick Reference
| Framework | Reference | Key features |
|-----------|-----------|-------------|
| pytest | `references/pytest.md` | Fixtures, parametrize, conftest, markers, coverage, async tests |
| Vitest | `references/vitest.md` | vi.mock, vi.fn, Testing Library, MSW, workspace, coverage |
| Jest | `references/jest.md` | jest.mock, jest.fn, @jest/globals, NestJS testing, migration to Vitest |
---
## Best Practices
1. **Name tests descriptively.** `test_[function]_[scenario]_[expected]` (Python) or `it('should [behavior]')` (JS/TS).
2. **Keep tests independent.** Never rely on execution order. Each test sets up its own state.
3. **One assertion focus per test.** Multiple asserts OK if verifying the same behavior.
4. **Mock at the boundary, not in the middle.** Mock external services, databases, and network calls. Don't mock internal functions.
5. **Clear/restore mocks between tests.** `vi.clearAllMocks()` in `beforeEach` or `jest.restoreAllMocks()` in `afterEach`.
6. **Use `userEvent` over `fireEvent`** for React component testing (simulates real user behavior).
7. **Query by role and label, not test IDs** (`getByRole`, `getByLabelText` over `getByTestId`).
8. **Run the full suite in CI with branch coverage.** Local development can use `-x` for fast feedback.
## Common Pitfalls
1. **Forgetting to `await` in async tests.** Omitting `await` makes tests pass vacuously.
2. **Mock hoisting confusion.** `vi.mock()`/`jest.mock()` calls are hoisted — variables referenced in mock implementations may be undefined.
3. **Shared mutable fixtures.** A module-scoped fixture returning a mutable object gets modified by one test and breaks another.
4. **Patching the wrong import path.** Patch where the import is looked up, not where it's defined.
5. **Snapshot overuse.** Developers update snapshots without reviewing diffs. Prefer explicit assertions.
6. **Not cleaning up fake timers.** Forgetting `vi.useRealTimers()` in `afterEach` breaks subsequent tests.
7. **Testing implementation, not behavior.** Assert on outcomes, not internal method calls.
8. **Running Jest where Vitest fits.** For new Vite/React/Next.js projects, Vitest is strictly better.
---
## Related Skills
- `testing-anti-patterns` — Common testing mistakes to avoid
- `test-driven-development` — TDD workflow
- `playwright` — End-to-end browser testing
- `languages` — Language-specific test idioms
+409
View File
@@ -0,0 +1,409 @@
# Testing — Jest Patterns
# Jest
## Overview
Testing patterns for projects that use Jest as their test runner — primarily NestJS backends and legacy React projects. For new TypeScript/React projects, prefer `vitest` (faster, native ESM, Vite-aligned). This skill focuses on Jest-specific patterns, NestJS integration, and the Jest-to-Vitest migration path.
## When to Use
- NestJS projects (Jest is the default test runner)
- Existing projects that already use Jest
- React component testing with Jest + Testing Library
- Debugging Jest configuration issues (ESM, TypeScript transforms, module resolution)
## When NOT to Use
- **New Vite/React/Next.js projects** — use `vitest` (better ESM support, faster)
- **Python testing** — use `pytest`
- **E2E browser testing** — use `playwright`
- **Cloudflare Workers** — use `vitest` with `@cloudflare/vitest-pool-workers`
---
## Quick Reference
| I need... | Go to |
|-----------|-------|
| NestJS testing patterns | § NestJS Testing below |
| Mock patterns | § Mocking below |
| TypeScript config | § Configuration below |
| ESM troubleshooting | § ESM Gotchas below |
| Migration to Vitest | § Jest → Vitest Migration below |
---
## Core Patterns
### Test structure
```typescript
import { describe, it, expect, beforeEach, afterEach, jest } from '@jest/globals';
describe('UserService', () => {
let service: UserService;
beforeEach(() => {
service = new UserService();
});
it('should create a user with default role', () => {
const user = service.create({ email: 'test@example.com', name: 'Test' });
expect(user.role).toBe('member');
});
it('should throw on duplicate email', () => {
service.create({ email: 'test@example.com', name: 'A' });
expect(() => service.create({ email: 'test@example.com', name: 'B' }))
.toThrow('Email already exists');
});
});
```
### Assertions
```typescript
// Equality
expect(result).toBe(42); // strict ===
expect(result).toEqual({ id: '1' }); // deep equality
expect(result).toStrictEqual(obj); // deep + type equality
// Truthiness
expect(value).toBeTruthy();
expect(value).toBeFalsy();
expect(value).toBeNull();
expect(value).toBeUndefined();
expect(value).toBeDefined();
// Numbers
expect(count).toBeGreaterThan(0);
expect(price).toBeCloseTo(9.99, 2);
// Strings
expect(message).toMatch(/error/i);
expect(message).toContain('failed');
// Arrays / objects
expect(arr).toContain('item');
expect(arr).toHaveLength(3);
expect(obj).toHaveProperty('email', 'test@example.com');
// Exceptions
expect(() => parse('{bad}')).toThrow(SyntaxError);
expect(() => validate({})).toThrow('Required');
// Async
await expect(fetchUser('missing')).rejects.toThrow('Not found');
await expect(fetchUser('exists')).resolves.toHaveProperty('id');
```
---
## Mocking
### `jest.fn()` — standalone mock function
```typescript
const callback = jest.fn();
callback('arg1');
expect(callback).toHaveBeenCalledTimes(1);
expect(callback).toHaveBeenCalledWith('arg1');
```
### `jest.spyOn()` — spy on existing methods
```typescript
const spy = jest.spyOn(service, 'findOne').mockResolvedValue(mockUser);
await controller.getUser('123');
expect(spy).toHaveBeenCalledWith('123');
spy.mockRestore(); // Restore original
```
### `jest.mock()` — module mocking
```typescript
// Auto-mock entire module
jest.mock('./email.service');
// Manual mock with implementation
jest.mock('./email.service', () => ({
EmailService: jest.fn().mockImplementation(() => ({
send: jest.fn().mockResolvedValue({ messageId: 'msg_123' }),
})),
}));
```
### Mock return values
```typescript
const mock = jest.fn();
mock.mockReturnValue(42); // Sync
mock.mockReturnValueOnce(1); // First call only
mock.mockResolvedValue({ ok: true }); // Async
mock.mockRejectedValue(new Error()); // Async throw
mock.mockImplementation((x) => x * 2); // Custom logic
```
### Clear vs Reset vs Restore
| Method | Clears calls | Resets implementation | Restores original |
|--------|-------------|----------------------|-------------------|
| `mockClear()` | yes | no | no |
| `mockReset()` | yes | yes (returns undefined) | no |
| `mockRestore()` | yes | yes | yes (spyOn only) |
Use `jest.restoreAllMocks()` in `afterEach` to avoid mock leaks.
---
## NestJS Testing
### Unit test a service
```typescript
import { Test, TestingModule } from '@nestjs/testing';
import { UsersService } from './users.service';
import { PrismaService } from '../prisma/prisma.service';
import { NotFoundException } from '@nestjs/common';
describe('UsersService', () => {
let service: UsersService;
let prisma: jest.Mocked<PrismaService>;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
UsersService,
{
provide: PrismaService,
useValue: {
user: {
findUnique: jest.fn(),
create: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
},
},
},
],
}).compile();
service = module.get(UsersService);
prisma = module.get(PrismaService);
});
it('throws NotFoundException for missing user', async () => {
prisma.user.findUnique.mockResolvedValue(null);
await expect(service.findOne('missing')).rejects.toThrow(NotFoundException);
});
it('returns user when found', async () => {
const mockUser = { id: '1', email: 'test@example.com', name: 'Test' };
prisma.user.findUnique.mockResolvedValue(mockUser);
const result = await service.findOne('1');
expect(result).toEqual(mockUser);
expect(prisma.user.findUnique).toHaveBeenCalledWith({ where: { id: '1' } });
});
});
```
### E2E test a controller
```typescript
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication, ValidationPipe } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
describe('Users (e2e)', () => {
let app: INestApplication;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = module.createNestApplication();
app.useGlobalPipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true }));
await app.init();
});
afterAll(() => app.close());
it('POST /users creates user', () =>
request(app.getHttpServer())
.post('/users')
.send({ email: 'test@example.com', name: 'Test' })
.expect(201)
.expect((res) => expect(res.body).toHaveProperty('id')));
it('POST /users rejects invalid payload', () =>
request(app.getHttpServer())
.post('/users')
.send({ email: 'bad' })
.expect(400));
});
```
### Test a guard
```typescript
import { ExecutionContext } from '@nestjs/common';
import { JwtAuthGuard } from './jwt-auth.guard';
import { JwtService } from '@nestjs/jwt';
describe('JwtAuthGuard', () => {
let guard: JwtAuthGuard;
let jwtService: jest.Mocked<JwtService>;
beforeEach(() => {
jwtService = { verifyAsync: jest.fn() } as any;
guard = new JwtAuthGuard(jwtService);
});
const mockContext = (authHeader?: string): ExecutionContext => ({
switchToHttp: () => ({
getRequest: () => ({
headers: { authorization: authHeader },
}),
}),
}) as any;
it('rejects missing token', async () => {
await expect(guard.canActivate(mockContext())).rejects.toThrow('Missing bearer token');
});
it('accepts valid token', async () => {
jwtService.verifyAsync.mockResolvedValue({ sub: 'user_1', role: 'admin' });
await expect(guard.canActivate(mockContext('Bearer valid.jwt.token'))).resolves.toBe(true);
});
});
```
---
## Configuration
### TypeScript with `ts-jest`
```typescript
// jest.config.ts
import type { Config } from 'jest';
const config: Config = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/src'],
testMatch: ['**/*.spec.ts', '**/*.test.ts'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
},
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.module.ts',
'!src/main.ts',
'!src/**/*.dto.ts',
'!src/**/*.entity.ts',
],
coverageThreshold: {
global: { branches: 80, functions: 80, lines: 80, statements: 80 },
},
};
export default config;
```
### SWC transform (faster)
Replace `ts-jest` with `@swc/jest` for 5-10x faster transforms:
```typescript
// jest.config.ts
const config: Config = {
transform: {
'^.+\\.tsx?$': ['@swc/jest'],
},
// ... rest same
};
```
### React + Testing Library
```typescript
// jest.config.ts
const config: Config = {
testEnvironment: 'jsdom',
setupFilesAfterSetup: ['<rootDir>/jest.setup.ts'],
transform: { '^.+\\.tsx?$': ['@swc/jest'] },
moduleNameMapper: {
'\\.(css|less|scss)$': 'identity-obj-proxy',
'\\.(jpg|png|svg)$': '<rootDir>/__mocks__/fileMock.ts',
},
};
// jest.setup.ts
import '@testing-library/jest-dom';
```
---
## ESM Gotchas
Jest's ESM support is still experimental. Common issues and fixes:
| Problem | Fix |
|---------|-----|
| `SyntaxError: Cannot use import` | Add `transform` with `ts-jest` or `@swc/jest` |
| Module not found for `.js` imports | Set `moduleNameMapper` or use `ts-jest` with `useESM: true` |
| `jest.mock()` doesn't work with ESM | Use `jest.unstable_mockModule()` (experimental) |
| Dynamic `import()` in tests | Set `transform` to handle the syntax |
| `__dirname` undefined | ESM doesn't have `__dirname`; use `import.meta.url` + `fileURLToPath` |
**If fighting ESM issues takes more than 30 minutes, migrate to Vitest.** Vitest handles ESM natively and is a near-drop-in replacement.
---
## Jest → Vitest Migration
For projects outgrowing Jest's ESM limitations or wanting faster transforms:
| Jest | Vitest |
|------|--------|
| `jest.fn()` | `vi.fn()` |
| `jest.mock('./mod')` | `vi.mock('./mod')` |
| `jest.spyOn(obj, 'method')` | `vi.spyOn(obj, 'method')` |
| `jest.useFakeTimers()` | `vi.useFakeTimers()` |
| `jest.config.ts` | `vitest.config.ts` |
| `@jest/globals` | `vitest` |
| `ts-jest` / `@swc/jest` | Not needed (native TS) |
| `jest.setup.ts``setupFilesAfterSetup` | `vitest.config.ts``setupFiles` |
Most tests migrate with a find-replace of `jest``vi` and `@jest/globals``vitest`. Run `npx vitest --reporter=verbose` to catch edge cases.
---
## Common Pitfalls
1. **Mock leaks between tests.** Always call `jest.restoreAllMocks()` in `afterEach`. Without it, one test's mock infects the next.
2. **Forgetting `await` on async assertions.** `expect(fn()).rejects.toThrow()` without `await` silently passes even if the promise resolves.
3. **Using `jest.mock()` with ESM.** Module-level `jest.mock()` doesn't work reliably with ESM. Use `jest.unstable_mockModule()` or switch to Vitest.
4. **Testing implementation, not behavior.** Asserting `mock.toHaveBeenCalledTimes(3)` tests internal calls, not outcomes. Assert on the return value or side effect instead.
5. **Slow transforms.** Default `ts-jest` is slow. Switch to `@swc/jest` for 5-10x speedup with zero config change.
6. **Not closing NestJS app in E2E tests.** Missing `afterAll(() => app.close())` leaks connections and causes "open handle" warnings.
7. **Snapshot overuse.** `toMatchSnapshot()` on large objects makes tests pass everything — any change auto-updates. Use targeted assertions instead.
8. **Running Jest where Vitest fits.** For new Vite/React/Next.js projects, Vitest is strictly better (native ESM, faster, same API). Only use Jest when the framework mandates it (NestJS) or the project already depends on it.
---
## Related Skills
- `vitest` — preferred runner for new TypeScript/React projects
- `nestjs` — NestJS framework (Jest is the default runner)
- `react` — React component patterns
- `testing-anti-patterns` — test quality pitfalls (applies to Jest too)
- `test-driven-development` — TDD methodology
+686
View File
@@ -0,0 +1,686 @@
# Testing — pytest Patterns
# pytest
## When to Use
- Writing Python tests
- Test fixtures and setup
- Mocking dependencies
## When NOT to Use
- JavaScript or TypeScript testing -- use the `vitest` skill instead
- Projects that explicitly mandate unittest-only by convention with no pytest dependency
- Non-Python test files or environments
---
## Core Patterns
### 1. Fixtures
Fixtures provide reusable setup and teardown logic. They are requested by name as test function parameters.
#### Function-Scoped Fixtures (default)
A new instance is created for every test that requests it.
```python
import pytest
from myapp.models import User
from myapp.db import Session
@pytest.fixture
def user():
"""Fresh user instance per test."""
return User(id=1, name="Alice", email="alice@example.com")
def test_user_display_name(user):
assert user.display_name() == "Alice"
def test_user_email_domain(user):
assert user.email_domain() == "example.com"
```
#### Class and Module Scope
Use broader scopes for expensive resources that are safe to share.
```python
@pytest.fixture(scope="class")
def api_client():
"""Shared across all tests in a test class."""
client = APIClient(base_url="http://testserver")
client.authenticate(token="test-token")
return client
@pytest.fixture(scope="module")
def database_schema():
"""Created once per test module, shared across all tests in the file."""
engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(engine)
yield engine
engine.dispose()
@pytest.fixture(scope="session")
def redis_connection():
"""Created once for the entire test session."""
conn = Redis(host="localhost", port=6379, db=15)
conn.flushdb()
yield conn
conn.flushdb()
conn.close()
```
#### Yield Fixtures for Teardown
`yield` separates setup from teardown. Code after `yield` runs after the test completes, even if the test fails.
```python
@pytest.fixture
def db_session():
session = Session()
session.begin()
yield session
session.rollback()
session.close()
@pytest.fixture
def temp_config(tmp_path):
config_file = tmp_path / "config.yaml"
config_file.write_text("debug: true\nlog_level: INFO\n")
yield config_file
# tmp_path is automatically cleaned up by pytest
```
#### Autouse Fixtures
Apply a fixture to every test automatically without requesting it by name.
```python
@pytest.fixture(autouse=True)
def reset_environment(monkeypatch):
"""Ensure each test starts with clean environment variables."""
monkeypatch.delenv("API_KEY", raising=False)
monkeypatch.delenv("DATABASE_URL", raising=False)
@pytest.fixture(autouse=True)
def freeze_time():
"""Pin time for deterministic tests."""
with freeze_time("2025-06-15T12:00:00Z"):
yield
```
#### Factory Fixtures
Return a factory function when tests need multiple instances with varying parameters.
```python
@pytest.fixture
def make_user():
"""Factory that creates users with sensible defaults."""
created = []
def _make_user(name="Test User", role="viewer", active=True):
user = User(name=name, role=role, active=active)
created.append(user)
return user
yield _make_user
# Teardown: clean up all created users
for u in created:
u.delete()
def test_admin_permissions(make_user):
admin = make_user(name="Admin", role="admin")
viewer = make_user(name="Viewer", role="viewer")
assert admin.can_delete_users() is True
assert viewer.can_delete_users() is False
```
#### Parametrized Fixtures with request.param
Run the same test against multiple fixture variants.
```python
@pytest.fixture(params=["sqlite", "postgresql"])
def db_engine(request):
"""Test against multiple database backends."""
if request.param == "sqlite":
engine = create_engine("sqlite:///:memory:")
elif request.param == "postgresql":
engine = create_engine("postgresql://test:test@localhost/testdb")
yield engine
engine.dispose()
def test_insert_and_query(db_engine):
# This test runs twice: once with sqlite, once with postgresql
with db_engine.connect() as conn:
conn.execute(text("CREATE TABLE t (id INT)"))
conn.execute(text("INSERT INTO t VALUES (1)"))
result = conn.execute(text("SELECT * FROM t")).fetchall()
assert len(result) == 1
```
---
### 2. Parametrize
#### Single Parameter
```python
@pytest.mark.parametrize("email", [
"user@example.com",
"admin@test.org",
"name+tag@domain.co.uk",
])
def test_valid_email_accepted(email):
assert is_valid_email(email) is True
```
#### Multiple Parameters
```python
@pytest.mark.parametrize("input_text, expected", [
("hello", "HELLO"),
("world", "WORLD"),
("", ""),
("already UPPER", "ALREADY UPPER"),
])
def test_uppercase(input_text, expected):
assert input_text.upper() == expected
```
#### Custom IDs for Readable Output
```python
@pytest.mark.parametrize("status_code, should_retry", [
pytest.param(200, False, id="success-no-retry"),
pytest.param(429, True, id="rate-limited-retry"),
pytest.param(500, True, id="server-error-retry"),
pytest.param(404, False, id="not-found-no-retry"),
])
def test_retry_logic(status_code, should_retry):
response = MockResponse(status_code=status_code)
assert should_retry_request(response) is should_retry
```
#### Indirect Parametrize
Pass parameters through a fixture rather than directly to the test.
```python
@pytest.fixture
def user_role(request):
"""Create a user with the given role."""
return User(name="Test", role=request.param)
@pytest.mark.parametrize("user_role", ["admin", "editor", "viewer"], indirect=True)
def test_dashboard_access(user_role):
if user_role.role == "admin":
assert user_role.can_access("/admin/dashboard") is True
else:
assert user_role.can_access("/admin/dashboard") is False
```
#### Stacking Parametrize Decorators
Creates the cartesian product of all parameter sets.
```python
@pytest.mark.parametrize("method", ["GET", "POST", "PUT", "DELETE"])
@pytest.mark.parametrize("auth", ["token", "session", "none"])
def test_endpoint_auth(method, auth):
# Runs 4 x 3 = 12 test cases
response = make_request(method=method, auth_type=auth)
if auth == "none":
assert response.status_code == 401
else:
assert response.status_code in (200, 201, 204)
```
---
### 3. Mocking
#### monkeypatch -- Environment Variables and Attributes
```python
def test_reads_api_key_from_env(monkeypatch):
monkeypatch.setenv("API_KEY", "test-key-12345")
config = load_config()
assert config.api_key == "test-key-12345"
def test_missing_api_key_raises(monkeypatch):
monkeypatch.delenv("API_KEY", raising=False)
with pytest.raises(ConfigError, match="API_KEY is required"):
load_config()
def test_override_attribute(monkeypatch):
monkeypatch.setattr("myapp.settings.MAX_RETRIES", 0)
assert retry_request(failing_url) is None # No retries attempted
def test_override_dict_item(monkeypatch):
monkeypatch.setitem(app_config, "timeout", 1)
assert app_config["timeout"] == 1
```
#### unittest.mock.patch
```python
from unittest.mock import patch, Mock, AsyncMock
@patch("myapp.services.payment.stripe.Charge.create")
def test_charge_customer(mock_charge):
mock_charge.return_value = Mock(id="ch_123", status="succeeded")
result = process_payment(amount=1000, currency="usd", token="tok_visa")
mock_charge.assert_called_once_with(
amount=1000, currency="usd", source="tok_visa"
)
assert result.charge_id == "ch_123"
@patch("myapp.services.email.send_email")
@patch("myapp.services.user.UserRepository.find_by_id")
def test_send_welcome_email(mock_find, mock_send):
mock_find.return_value = User(id=1, email="new@example.com")
mock_send.return_value = True
send_welcome(user_id=1)
mock_send.assert_called_once_with(
to="new@example.com", template="welcome"
)
```
#### responses Library for HTTP Mocking
```python
import responses
import requests
@responses.activate
def test_fetch_user_from_api():
responses.add(
responses.GET,
"https://api.example.com/users/1",
json={"id": 1, "name": "Alice"},
status=200,
)
result = fetch_user(user_id=1)
assert result["name"] == "Alice"
assert len(responses.calls) == 1
assert responses.calls[0].request.url == "https://api.example.com/users/1"
@responses.activate
def test_api_timeout_handling():
responses.add(
responses.GET,
"https://api.example.com/users/1",
body=requests.exceptions.ConnectionError("Connection timed out"),
)
with pytest.raises(ServiceUnavailableError):
fetch_user(user_id=1)
```
#### pytest-mock's mocker Fixture
```python
def test_with_mocker(mocker):
mock_repo = mocker.patch("myapp.services.OrderRepository")
mock_repo.return_value.get_by_id.return_value = Order(
id=1, status="pending"
)
service = OrderService()
order = service.get_order(1)
assert order.status == "pending"
mock_repo.return_value.get_by_id.assert_called_once_with(1)
def test_spy_on_method(mocker):
spy = mocker.spy(UserService, "validate_email")
service = UserService()
service.register("alice@example.com")
spy.assert_called_once_with(service, "alice@example.com")
```
---
### 4. Async Testing
#### pytest-asyncio Basics
```python
import pytest
import httpx
@pytest.mark.asyncio
async def test_async_fetch():
async with httpx.AsyncClient() as client:
response = await client.get("https://httpbin.org/get")
assert response.status_code == 200
@pytest.mark.asyncio
async def test_async_exception():
with pytest.raises(ValueError, match="invalid"):
await validate_async_input("")
```
#### Async Fixtures
```python
@pytest.fixture
async def async_db_session():
session = AsyncSession(bind=async_engine)
await session.begin()
yield session
await session.rollback()
await session.close()
@pytest.mark.asyncio
async def test_async_query(async_db_session):
result = await async_db_session.execute(
select(User).where(User.active == True)
)
users = result.scalars().all()
assert len(users) >= 0
```
#### Configuring asyncio Mode
In `pyproject.toml` or `pytest.ini`, set the default mode to avoid repeating the marker:
```toml
# pyproject.toml
[tool.pytest.ini_options]
asyncio_mode = "auto"
```
With `asyncio_mode = "auto"`, any `async def test_*` function is automatically treated as async -- no `@pytest.mark.asyncio` needed.
---
### 5. Test Organization
#### conftest.py Hierarchy
```
tests/
├── conftest.py # Session/global fixtures (db connection, app client)
├── unit/
│ ├── conftest.py # Unit-specific fixtures (mocked services)
│ ├── test_models.py
│ └── test_utils.py
├── integration/
│ ├── conftest.py # Integration fixtures (real db session, test server)
│ ├── test_api.py
│ └── test_repositories.py
└── e2e/
├── conftest.py # E2E fixtures (browser, full app)
└── test_workflows.py
```
Fixtures in a `conftest.py` are available to all tests in the same directory and below. No imports needed.
#### Test Discovery
pytest discovers tests by default based on these rules:
- Files matching `test_*.py` or `*_test.py`
- Classes prefixed with `Test` (no `__init__` method)
- Functions prefixed with `test_`
Configure custom discovery in `pyproject.toml`:
```toml
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
```
#### Markers
```python
import pytest
import sys
# Built-in markers
@pytest.mark.skip(reason="Not implemented yet")
def test_future_feature():
pass
@pytest.mark.skipif(
sys.platform == "win32", reason="Unix-only functionality"
)
def test_unix_permissions():
pass
@pytest.mark.xfail(reason="Known bug #1234, fix pending")
def test_known_broken():
result = buggy_function()
assert result == "expected"
```
#### Custom Markers
Register markers in `pyproject.toml` to avoid warnings:
```toml
[tool.pytest.ini_options]
markers = [
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
"integration: marks integration tests requiring external services",
"smoke: critical path tests for quick validation",
]
```
```python
@pytest.mark.slow
def test_full_data_migration():
migrate_all_records() # Takes 30+ seconds
assert count_records() == EXPECTED_TOTAL
@pytest.mark.smoke
def test_health_endpoint(client):
response = client.get("/health")
assert response.status_code == 200
```
Run selectively:
```bash
pytest -m "smoke" # Only smoke tests
pytest -m "not slow" # Skip slow tests
pytest -m "integration and not slow" # Integration but not slow
```
---
### 6. Coverage
#### Basic Usage
```bash
pytest --cov=src --cov-report=term-missing
pytest --cov=src --cov-report=html # Generates htmlcov/
pytest --cov=src --cov-branch # Enable branch coverage
```
#### Configuration in pyproject.toml
```toml
[tool.pytest.ini_options]
addopts = "--cov=src --cov-report=term-missing --cov-fail-under=80"
[tool.coverage.run]
source = ["src"]
branch = true
omit = [
"*/migrations/*",
"*/tests/*",
"*/__pycache__/*",
"*/conftest.py",
]
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"def __repr__",
"if TYPE_CHECKING:",
"raise NotImplementedError",
"@overload",
]
fail_under = 80
show_missing = true
```
#### .coveragerc Alternative
If not using `pyproject.toml`, create `.coveragerc`:
```ini
[run]
source = src
branch = true
[report]
fail_under = 80
show_missing = true
exclude_lines =
pragma: no cover
def __repr__
if TYPE_CHECKING:
```
---
### 7. Assertions
#### pytest.raises for Exceptions
```python
def test_raises_value_error():
with pytest.raises(ValueError) as exc_info:
parse_age("not-a-number")
assert "invalid literal" in str(exc_info.value)
def test_raises_with_match():
with pytest.raises(PermissionError, match=r"User .+ lacks role 'admin'"):
authorize(user=viewer, required_role="admin")
```
#### pytest.approx for Floating Point
```python
def test_circle_area():
assert calculate_area(radius=5) == pytest.approx(78.5398, rel=1e-4)
def test_approx_list():
result = distribute_evenly(total=100, buckets=3)
assert result == pytest.approx([33.33, 33.33, 33.34], abs=0.01)
```
#### Custom Assertion Helpers
Build reusable assertion logic for domain-specific validation.
```python
def assert_valid_api_response(response, expected_status=200):
"""Reusable assertion for API responses."""
assert response.status_code == expected_status, (
f"Expected {expected_status}, got {response.status_code}: "
f"{response.text}"
)
data = response.json()
assert "error" not in data, f"Unexpected error: {data['error']}"
return data
def test_create_user(client):
response = client.post("/users", json={"name": "Alice"})
data = assert_valid_api_response(response, expected_status=201)
assert data["name"] == "Alice"
assert "id" in data
```
---
## Best Practices
1. **Name tests descriptively** -- Use `test_[function]_[scenario]_[expected]` so failures are self-explanatory without reading the test body. `test_parse_date_invalid_format_raises_valueerror` tells you everything.
2. **Keep tests independent** -- Never rely on test execution order. Each test should set up its own state via fixtures and tear it down afterward. Shared mutable state between tests is the top cause of flaky suites.
3. **One assertion focus per test** -- A test can have multiple `assert` statements, but they should all verify the same behavior. If you need to check two independent behaviors, write two tests.
4. **Use fixtures over setup methods** -- Prefer composable fixtures over `setUp`/`tearDown` methods or `setup_function`. Fixtures are explicit about dependencies, reusable across files via `conftest.py`, and support scoping.
5. **Mock at the boundary, not in the middle** -- Mock external services, databases, and network calls. Do not mock internal functions unless they are truly expensive. Over-mocking produces tests that pass but verify nothing.
6. **Use `tmp_path` for file operations** -- pytest's built-in `tmp_path` fixture provides a unique temporary directory per test. Never write to the real filesystem in tests.
7. **Pin randomness and time** -- When testing code that depends on randomness or the current time, use `random.seed()` or a time-freezing library to make tests deterministic.
8. **Run the full suite in CI with branch coverage** -- Local development can use `pytest -x` for fast feedback (stop on first failure), but CI must run the full suite with `--cov-branch` to catch untested branches and regressions.
---
## Common Pitfalls
1. **Shared mutable fixtures** -- A module-scoped fixture returning a mutable object (list, dict, instance) gets modified by one test and breaks another. Return fresh copies or use function scope for mutable data.
2. **Patching the wrong import path** -- `@patch("myapp.services.requests.get")` patches where `requests.get` is looked up, not where it is defined. If `services.py` does `from requests import get`, you must patch `myapp.services.get`, not `requests.get`.
3. **Forgetting to await in async tests** -- Omitting `await` makes the test pass vacuously because it never actually runs the coroutine. Always `await` the function under test and use `@pytest.mark.asyncio`.
4. **Tests that depend on execution order** -- If test B relies on side effects from test A, parallel test execution (pytest-xdist) and `--randomly` will expose the coupling immediately. Fix by making each test self-contained.
5. **Asserting on mock call count without checking arguments** -- `mock.assert_called_once()` confirms the call count but not what was passed. Use `assert_called_once_with(...)` or inspect `mock.call_args` to verify the actual arguments.
6. **Ignoring warnings as errors** -- Configure `filterwarnings = ["error"]` in `pyproject.toml` to catch deprecation warnings early. A passing test suite that emits 50 deprecation warnings is a time bomb.
---
## Related Skills
- `vitest` -- JavaScript/TypeScript testing counterpart
- `python` -- Python language patterns and idioms
- `test-driven-development` -- TDD workflow for writing tests first
- `github-actions` — Running pytest in CI/CD pipelines
+842
View File
@@ -0,0 +1,842 @@
# Testing — Vitest Patterns
# Vitest
## When to Use
- Testing JavaScript/TypeScript
- React component testing
- Unit and integration tests
## When NOT to Use
- Python testing -- use the `pytest` skill instead
- Projects that explicitly mandate Jest-only by convention with no Vitest dependency
- Non-JavaScript/TypeScript projects
---
## Core Patterns
### 1. Test Structure
#### describe / it / expect
```typescript
import { describe, it, expect } from 'vitest';
import { formatCurrency } from './format';
describe('formatCurrency', () => {
it('should format whole dollars', () => {
expect(formatCurrency(100)).toBe('$100.00');
});
it('should format cents correctly', () => {
expect(formatCurrency(9.5)).toBe('$9.50');
});
it('should handle zero', () => {
expect(formatCurrency(0)).toBe('$0.00');
});
it('should throw on negative values', () => {
expect(() => formatCurrency(-5)).toThrow('Amount must be non-negative');
});
});
```
#### Lifecycle Hooks
```typescript
import { describe, it, expect, beforeEach, afterEach, beforeAll, afterAll } from 'vitest';
import { Database } from './database';
describe('UserRepository', () => {
let db: Database;
beforeAll(async () => {
// Runs once before all tests in this describe block
db = await Database.connect('test://localhost/testdb');
await db.migrate();
});
afterAll(async () => {
await db.disconnect();
});
beforeEach(async () => {
// Runs before each test
await db.seed({ users: [{ id: 1, name: 'Alice' }] });
});
afterEach(async () => {
await db.truncate('users');
});
it('should find user by id', async () => {
const user = await db.users.findById(1);
expect(user).toEqual({ id: 1, name: 'Alice' });
});
it('should return null for missing user', async () => {
const user = await db.users.findById(999);
expect(user).toBeNull();
});
});
```
#### test.each for Parametrized Tests
```typescript
import { describe, it, expect, test } from 'vitest';
import { validateEmail } from './validators';
describe('validateEmail', () => {
test.each([
{ email: 'user@example.com', expected: true },
{ email: 'admin@test.org', expected: true },
{ email: 'name+tag@domain.co.uk', expected: true },
])('should accept valid email: $email', ({ email, expected }) => {
expect(validateEmail(email)).toBe(expected);
});
test.each([
{ email: '', reason: 'empty string' },
{ email: 'no-at-sign', reason: 'missing @' },
{ email: '@no-local.com', reason: 'missing local part' },
{ email: 'spaces in@email.com', reason: 'contains spaces' },
])('should reject invalid email ($reason): $email', ({ email }) => {
expect(validateEmail(email)).toBe(false);
});
});
```
#### Nested describe Blocks
```typescript
describe('ShoppingCart', () => {
describe('when empty', () => {
it('should have zero total', () => {
const cart = new ShoppingCart();
expect(cart.total()).toBe(0);
});
it('should have zero item count', () => {
const cart = new ShoppingCart();
expect(cart.itemCount()).toBe(0);
});
});
describe('with items', () => {
let cart: ShoppingCart;
beforeEach(() => {
cart = new ShoppingCart();
cart.add({ name: 'Widget', price: 9.99, quantity: 2 });
cart.add({ name: 'Gadget', price: 24.99, quantity: 1 });
});
it('should calculate total', () => {
expect(cart.total()).toBeCloseTo(44.97);
});
it('should count all items', () => {
expect(cart.itemCount()).toBe(3);
});
});
});
```
---
### 2. Mocking
#### vi.mock for Module Mocking
```typescript
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { sendWelcomeEmail } from './onboarding';
// Mock the entire email module -- hoisted to the top of the file automatically
vi.mock('./email', () => ({
sendEmail: vi.fn().mockResolvedValue({ messageId: 'msg-123' }),
}));
// Import AFTER vi.mock declaration
import { sendEmail } from './email';
describe('sendWelcomeEmail', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('should send email with welcome template', async () => {
await sendWelcomeEmail('alice@example.com');
expect(sendEmail).toHaveBeenCalledWith({
to: 'alice@example.com',
template: 'welcome',
subject: 'Welcome to our platform!',
});
});
it('should return the message id', async () => {
const result = await sendWelcomeEmail('alice@example.com');
expect(result.messageId).toBe('msg-123');
});
});
```
#### vi.fn for Function Spies
```typescript
import { describe, it, expect, vi } from 'vitest';
describe('EventEmitter', () => {
it('should call listener on emit', () => {
const emitter = new EventEmitter();
const listener = vi.fn();
emitter.on('click', listener);
emitter.emit('click', { x: 10, y: 20 });
expect(listener).toHaveBeenCalledOnce();
expect(listener).toHaveBeenCalledWith({ x: 10, y: 20 });
});
it('should track multiple calls', () => {
const callback = vi.fn();
callback('first');
callback('second');
callback('third');
expect(callback).toHaveBeenCalledTimes(3);
expect(callback.mock.calls).toEqual([['first'], ['second'], ['third']]);
});
});
```
#### vi.spyOn
```typescript
import { describe, it, expect, vi, afterEach } from 'vitest';
import * as mathUtils from './math-utils';
describe('calculateTax', () => {
afterEach(() => {
vi.restoreAllMocks();
});
it('should use the tax rate function', () => {
const spy = vi.spyOn(mathUtils, 'getTaxRate').mockReturnValue(0.08);
const result = calculateTax(100);
expect(spy).toHaveBeenCalledWith();
expect(result).toBe(8);
});
it('should spy without changing behavior', () => {
const spy = vi.spyOn(console, 'warn');
triggerDeprecationWarning();
expect(spy).toHaveBeenCalledWith(
expect.stringContaining('deprecated')
);
});
});
```
#### mockResolvedValue / mockRejectedValue
```typescript
import { describe, it, expect, vi } from 'vitest';
describe('UserService', () => {
it('should return user on successful fetch', async () => {
const fetchUser = vi.fn().mockResolvedValue({ id: 1, name: 'Alice' });
const user = await fetchUser(1);
expect(user).toEqual({ id: 1, name: 'Alice' });
});
it('should throw on failed fetch', async () => {
const fetchUser = vi.fn().mockRejectedValue(new Error('User not found'));
await expect(fetchUser(999)).rejects.toThrow('User not found');
});
it('should return different values on successive calls', async () => {
const getToken = vi.fn()
.mockResolvedValueOnce('token-1')
.mockResolvedValueOnce('token-2')
.mockRejectedValueOnce(new Error('Expired'));
expect(await getToken()).toBe('token-1');
expect(await getToken()).toBe('token-2');
await expect(getToken()).rejects.toThrow('Expired');
});
});
```
#### MSW (Mock Service Worker) for API Mocking
```typescript
import { describe, it, expect, beforeAll, afterAll, afterEach } from 'vitest';
import { setupServer } from 'msw/node';
import { http, HttpResponse } from 'msw';
import { fetchUsers } from './api-client';
const server = setupServer(
http.get('https://api.example.com/users', () => {
return HttpResponse.json([
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
]);
}),
http.post('https://api.example.com/users', async ({ request }) => {
const body = await request.json() as { name: string };
return HttpResponse.json(
{ id: 3, name: body.name },
{ status: 201 }
);
})
);
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
describe('API Client', () => {
it('should fetch users', async () => {
const users = await fetchUsers();
expect(users).toHaveLength(2);
expect(users[0].name).toBe('Alice');
});
it('should handle server errors', async () => {
server.use(
http.get('https://api.example.com/users', () => {
return HttpResponse.json(
{ message: 'Internal Server Error' },
{ status: 500 }
);
})
);
await expect(fetchUsers()).rejects.toThrow('Server error');
});
});
```
---
### 3. React Testing
#### Render and Query
```tsx
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import { Greeting } from './Greeting';
describe('Greeting', () => {
it('should display the user name', () => {
render(<Greeting name="Alice" />);
// getBy* throws if not found -- use for elements that must exist
expect(screen.getByText('Hello, Alice!')).toBeInTheDocument();
});
it('should not display admin badge for regular users', () => {
render(<Greeting name="Alice" role="viewer" />);
// queryBy* returns null if not found -- use for asserting absence
expect(screen.queryByText('Admin')).not.toBeInTheDocument();
});
it('should display admin badge for admins', () => {
render(<Greeting name="Alice" role="admin" />);
expect(screen.getByText('Admin')).toBeInTheDocument();
});
});
```
#### userEvent for Interactions
```tsx
import { describe, it, expect, vi } from 'vitest';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { LoginForm } from './LoginForm';
describe('LoginForm', () => {
it('should submit credentials', async () => {
const user = userEvent.setup();
const onSubmit = vi.fn();
render(<LoginForm onSubmit={onSubmit} />);
await user.type(screen.getByLabelText('Email'), 'alice@example.com');
await user.type(screen.getByLabelText('Password'), 'secret123');
await user.click(screen.getByRole('button', { name: 'Sign In' }));
expect(onSubmit).toHaveBeenCalledWith({
email: 'alice@example.com',
password: 'secret123',
});
});
it('should show validation error on empty submit', async () => {
const user = userEvent.setup();
render(<LoginForm onSubmit={vi.fn()} />);
await user.click(screen.getByRole('button', { name: 'Sign In' }));
expect(screen.getByText('Email is required')).toBeInTheDocument();
});
it('should toggle password visibility', async () => {
const user = userEvent.setup();
render(<LoginForm onSubmit={vi.fn()} />);
const passwordInput = screen.getByLabelText('Password');
expect(passwordInput).toHaveAttribute('type', 'password');
await user.click(screen.getByRole('button', { name: 'Show password' }));
expect(passwordInput).toHaveAttribute('type', 'text');
});
});
```
#### findBy for Async Rendering and waitFor
```tsx
import { describe, it, expect, vi } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { UserProfile } from './UserProfile';
describe('UserProfile', () => {
it('should load and display user data', async () => {
render(<UserProfile userId={1} />);
// findBy* waits for the element to appear (async query)
const heading = await screen.findByRole('heading', { name: 'Alice' });
expect(heading).toBeInTheDocument();
});
it('should show loading state initially', () => {
render(<UserProfile userId={1} />);
expect(screen.getByText('Loading...')).toBeInTheDocument();
});
it('should update after action', async () => {
const user = userEvent.setup();
render(<UserProfile userId={1} />);
await screen.findByRole('heading', { name: 'Alice' });
await user.click(screen.getByRole('button', { name: 'Deactivate' }));
await waitFor(() => {
expect(screen.getByText('Status: Inactive')).toBeInTheDocument();
});
});
});
```
#### Testing with Context Providers
```tsx
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import { ThemeProvider } from './ThemeContext';
import { ThemedButton } from './ThemedButton';
function renderWithProviders(ui: React.ReactElement, options?: { theme?: 'light' | 'dark' }) {
const theme = options?.theme ?? 'light';
return render(
<ThemeProvider value={theme}>
{ui}
</ThemeProvider>
);
}
describe('ThemedButton', () => {
it('should apply light theme styles', () => {
renderWithProviders(<ThemedButton>Click me</ThemedButton>, { theme: 'light' });
expect(screen.getByRole('button')).toHaveClass('btn-light');
});
it('should apply dark theme styles', () => {
renderWithProviders(<ThemedButton>Click me</ThemedButton>, { theme: 'dark' });
expect(screen.getByRole('button')).toHaveClass('btn-dark');
});
});
```
---
### 4. Async Testing
#### Promises and async/await
```typescript
import { describe, it, expect } from 'vitest';
import { fetchUser, processQueue } from './services';
describe('async operations', () => {
it('should resolve with user data', async () => {
const user = await fetchUser(1);
expect(user).toEqual({ id: 1, name: 'Alice' });
});
it('should reject with descriptive error', async () => {
await expect(fetchUser(-1)).rejects.toThrow('Invalid user ID');
});
it('should process all items in queue', async () => {
const results = await processQueue(['a', 'b', 'c']);
expect(results).toHaveLength(3);
expect(results.every((r) => r.status === 'done')).toBe(true);
});
});
```
#### Fake Timers
```typescript
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { debounce } from './debounce';
describe('debounce', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it('should not call function before delay', () => {
const fn = vi.fn();
const debounced = debounce(fn, 300);
debounced();
vi.advanceTimersByTime(200);
expect(fn).not.toHaveBeenCalled();
});
it('should call function after delay', () => {
const fn = vi.fn();
const debounced = debounce(fn, 300);
debounced();
vi.advanceTimersByTime(300);
expect(fn).toHaveBeenCalledOnce();
});
it('should reset timer on subsequent calls', () => {
const fn = vi.fn();
const debounced = debounce(fn, 300);
debounced();
vi.advanceTimersByTime(200);
debounced(); // reset
vi.advanceTimersByTime(200);
expect(fn).not.toHaveBeenCalled();
vi.advanceTimersByTime(100);
expect(fn).toHaveBeenCalledOnce();
});
});
```
#### Fake Timers with Date
```typescript
import { describe, it, expect, vi } from 'vitest';
import { isExpired } from './token';
describe('isExpired', () => {
it('should detect expired tokens', () => {
vi.useFakeTimers();
vi.setSystemTime(new Date('2025-06-15T12:00:00Z'));
const token = { expiresAt: '2025-06-15T11:00:00Z' };
expect(isExpired(token)).toBe(true);
vi.useRealTimers();
});
});
```
---
### 5. Snapshot Testing
#### toMatchSnapshot
```typescript
import { describe, it, expect } from 'vitest';
import { render } from '@testing-library/react';
import { Badge } from './Badge';
describe('Badge', () => {
it('should match snapshot for success variant', () => {
const { container } = render(<Badge variant="success">Active</Badge>);
expect(container.firstChild).toMatchSnapshot();
});
});
```
#### toMatchInlineSnapshot
Inline snapshots embed the expected value directly in the test file. Vitest updates them automatically on first run.
```typescript
import { describe, it, expect } from 'vitest';
import { formatError } from './errors';
describe('formatError', () => {
it('should format validation error', () => {
const error = formatError({ field: 'email', rule: 'required' });
expect(error).toMatchInlineSnapshot(`
{
"code": "VALIDATION_ERROR",
"field": "email",
"message": "email is required",
}
`);
});
});
```
#### When to Use Snapshots (and When Not To)
**Use snapshots for:**
- Serialized output that is tedious to write by hand (large objects, rendered markup)
- Catching unintended changes in generated output
- Error message formatting
**Do not use snapshots for:**
- Business logic assertions -- write explicit `expect(value).toBe(expected)` instead
- Frequently changing output -- snapshot churn leads to mindless updates
- Large component trees -- a small change deep in the tree makes the diff unreadable; test specific elements instead
---
### 6. Coverage
#### vitest.config.ts Coverage Settings
```typescript
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
coverage: {
provider: 'v8', // or 'istanbul'
reporter: ['text', 'html', 'lcov'],
reportsDirectory: './coverage',
include: ['src/**/*.{ts,tsx}'],
exclude: [
'src/**/*.test.{ts,tsx}',
'src/**/*.d.ts',
'src/**/index.ts', // barrel files
'src/test-utils/**',
],
thresholds: {
statements: 80,
branches: 80,
functions: 80,
lines: 80,
},
},
},
});
```
#### Running Coverage
```bash
vitest run --coverage # Run once with coverage
vitest --coverage # Watch mode with coverage
vitest run --coverage.provider=v8 # Override provider via CLI
```
#### Per-File Thresholds
```typescript
// vitest.config.ts
export default defineConfig({
test: {
coverage: {
provider: 'v8',
thresholds: {
// Global thresholds
statements: 80,
// Per-glob overrides for critical paths
'src/auth/**': {
statements: 95,
branches: 95,
},
},
},
},
});
```
---
### 7. Setup and Configuration
#### vitest.config.ts Basics
```typescript
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
test: {
globals: true, // Use describe/it/expect without imports
environment: 'jsdom', // DOM environment for React (or 'happy-dom')
setupFiles: ['./src/test-setup.ts'],
include: ['src/**/*.test.{ts,tsx}'],
exclude: ['node_modules', 'dist', 'e2e'],
testTimeout: 10_000,
hookTimeout: 30_000,
},
resolve: {
alias: {
'@': '/src',
},
},
});
```
#### Setup File
```typescript
// src/test-setup.ts
import '@testing-library/jest-dom/vitest';
import { cleanup } from '@testing-library/react';
import { afterEach } from 'vitest';
// Automatic cleanup after each test
afterEach(() => {
cleanup();
});
```
#### Workspace Configuration
For monorepos with multiple packages:
```typescript
// vitest.workspace.ts
import { defineWorkspace } from 'vitest/config';
export default defineWorkspace([
{
extends: './vitest.config.ts',
test: {
name: 'ui',
include: ['packages/ui/**/*.test.{ts,tsx}'],
environment: 'jsdom',
},
},
{
extends: './vitest.config.ts',
test: {
name: 'api',
include: ['packages/api/**/*.test.ts'],
environment: 'node',
},
},
]);
```
#### Environment Per File
Use a magic comment at the top of a test file to override the environment:
```typescript
// @vitest-environment happy-dom
import { describe, it, expect } from 'vitest';
describe('DOM-heavy tests', () => {
it('should create elements', () => {
const div = document.createElement('div');
div.textContent = 'Hello';
expect(div.textContent).toBe('Hello');
});
});
```
#### Globals Mode
When `globals: true` is set in config, you do not need to import `describe`, `it`, `expect`, `vi`, etc. Add the types to `tsconfig.json`:
```json
{
"compilerOptions": {
"types": ["vitest/globals"]
}
}
```
---
## Best Practices
1. **Use `userEvent` over `fireEvent`** -- `userEvent` simulates real user behavior (focus, keystrokes, blur) while `fireEvent` dispatches raw DOM events. `userEvent` catches bugs that `fireEvent` misses, such as disabled buttons still receiving clicks.
2. **Query by role and label, not test IDs** -- Prefer `getByRole('button', { name: 'Submit' })` and `getByLabelText('Email')` over `getByTestId('submit-btn')`. Accessible queries validate your markup and are resilient to refactors.
3. **Clear mocks between tests** -- Call `vi.clearAllMocks()` in `beforeEach` or `vi.restoreAllMocks()` in `afterEach`. Leaked mock state between tests causes order-dependent failures that are painful to debug.
4. **Keep tests focused on one behavior** -- Each `it` block should test a single user-observable behavior. If your test description contains "and", split it into two tests.
5. **Avoid testing implementation details** -- Do not assert on component state, internal method calls, or private variables. Test what the user sees and what the component outputs. Implementation tests break on every refactor without catching real bugs.
6. **Use MSW for network mocking over vi.mock on fetch** -- MSW intercepts at the network level, so your tests exercise the actual fetch/axios code paths. Mocking `fetch` directly skips serialization, headers, and error handling logic.
7. **Colocate tests with source files** -- Place `Button.test.tsx` next to `Button.tsx`. This makes it obvious which files have tests and simplifies imports. Reserve a top-level `e2e/` folder only for end-to-end tests.
8. **Run tests in watch mode during development** -- `vitest` (no flags) starts in watch mode and re-runs only affected tests on file change. Use `vitest run` in CI for a single full run with exit code.
---
## Common Pitfalls
1. **Forgetting to await userEvent calls** -- Every `userEvent` method is async. Omitting `await` causes the assertion to run before the interaction completes, leading to false passes or intermittent failures.
2. **vi.mock hoisting confusion** -- `vi.mock()` calls are hoisted to the top of the file. If you define a mock implementation that references a variable declared below the `vi.mock` call, it will be `undefined`. Use `vi.mock` with a factory function or move the variable above.
3. **Not cleaning up after fake timers** -- Forgetting `vi.useRealTimers()` in `afterEach` causes subsequent tests to silently use fake timers, producing mysterious timeouts and passing tests that should fail.
4. **Using `getBy` queries for elements that may not exist** -- `getByText('Error')` throws immediately if the element is absent. When asserting that something is NOT rendered, use `queryByText('Error')` which returns `null`.
5. **Snapshot overuse** -- Developers update snapshots without reviewing the diff. Over time, snapshots become rubber stamps. Limit snapshots to serialized output and error formatting; use explicit assertions for behavior.
6. **Testing third-party library internals** -- Do not test that React Router navigates correctly or that Zustand updates state. Test that your component renders the right thing after navigation or state change. Trust library authors; test your code.
---
## Related Skills
- `pytest` -- Python testing counterpart
- `typescript` -- TypeScript language patterns and strict typing
- `react` -- React component patterns for component testing
- `test-driven-development` -- TDD workflow for writing tests first
- `github-actions` — Running vitest in CI/CD pipelines