feat: enhanced the writing skills

This commit is contained in:
duthaho
2026-04-18 18:50:39 +07:00
parent 7fa9a48c6c
commit 09538078e7
136 changed files with 10175 additions and 7947 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
@@ -1,248 +0,0 @@
# pytest Fixture Patterns
Catalog of reusable fixture patterns for common testing scenarios.
## 1. Factory Fixture
Create multiple instances with customizable defaults.
```python
import pytest
from dataclasses import dataclass
@pytest.fixture
def make_user():
"""Factory fixture: creates User instances with sensible defaults."""
created = []
def _make_user(
name: str = "Test User",
email: str | None = None,
is_active: bool = True,
):
if email is None:
email = f"user-{len(created)}@test.com"
user = User(name=name, email=email, is_active=is_active)
created.append(user)
return user
yield _make_user
# Cleanup: delete all created users
for user in created:
user.delete()
```
Usage:
```python
def test_deactivate_user(make_user):
user = make_user(name="Alice", is_active=True)
user.deactivate()
assert not user.is_active
```
## 2. Database Session (SQLAlchemy)
Transaction-isolated database session that rolls back after each test.
```python
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
@pytest.fixture(scope="session")
def engine():
"""Create a test database engine (once per test session)."""
engine = create_engine("postgresql://test:test@localhost:5432/test_db")
yield engine
engine.dispose()
@pytest.fixture(scope="session")
def tables(engine):
"""Create all tables once, drop after all tests."""
Base.metadata.create_all(engine)
yield
Base.metadata.drop_all(engine)
@pytest.fixture
def db_session(engine, tables):
"""Provide a transactional database session that rolls back after each test."""
connection = engine.connect()
transaction = connection.begin()
session = sessionmaker(bind=connection)()
yield session
session.close()
transaction.rollback()
connection.close()
```
## 3. Temporary Files and Directories
```python
@pytest.fixture
def sample_config(tmp_path: Path) -> Path:
"""Create a temporary config file with test content."""
config = tmp_path / "config.yaml"
config.write_text(
"""\
database:
host: localhost
port: 5432
debug: true
"""
)
return config
@pytest.fixture
def data_dir(tmp_path: Path) -> Path:
"""Create a temporary directory structure for testing."""
(tmp_path / "input").mkdir()
(tmp_path / "output").mkdir()
(tmp_path / "input" / "data.csv").write_text("id,name\n1,Alice\n2,Bob\n")
return tmp_path
```
## 4. Mock External Service
```python
import pytest
from unittest.mock import AsyncMock, MagicMock, patch
@pytest.fixture
def mock_http_client():
"""Mock an HTTP client with pre-configured responses."""
client = MagicMock()
client.get.return_value = MagicMock(
status_code=200,
json=lambda: {"status": "ok"},
)
client.post.return_value = MagicMock(
status_code=201,
json=lambda: {"id": "new-123"},
)
return client
@pytest.fixture
def mock_payment_gateway():
"""Mock a payment gateway service."""
with patch("app.services.payment.PaymentGateway") as mock_cls:
instance = mock_cls.return_value
instance.charge.return_value = {
"transaction_id": "txn-test-123",
"status": "succeeded",
}
instance.refund.return_value = {
"refund_id": "ref-test-456",
"status": "refunded",
}
yield instance
# Async version
@pytest.fixture
def mock_email_service():
"""Mock an async email service."""
with patch("app.services.email.EmailService") as mock_cls:
instance = mock_cls.return_value
instance.send = AsyncMock(return_value={"message_id": "msg-test-789"})
yield instance
```
## 5. Authenticated Test Client (FastAPI)
```python
import pytest
from httpx import AsyncClient, ASGITransport
from app.main import app
from app.auth import create_access_token
@pytest.fixture
def auth_token():
"""Generate a valid JWT token for testing."""
return create_access_token(
data={"sub": "test-user-id", "role": "admin"},
expires_minutes=60,
)
@pytest.fixture
def auth_headers(auth_token: str) -> dict[str, str]:
"""HTTP headers with Bearer token."""
return {"Authorization": f"Bearer {auth_token}"}
@pytest.fixture
async def client():
"""Unauthenticated async test client."""
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as c:
yield c
@pytest.fixture
async def auth_client(auth_headers):
"""Authenticated async test client."""
transport = ASGITransport(app=app)
async with AsyncClient(
transport=transport,
base_url="http://test",
headers=auth_headers,
) as c:
yield c
```
## 6. Environment Variables
```python
@pytest.fixture
def env_vars(monkeypatch):
"""Set environment variables for the test, automatically restored after."""
monkeypatch.setenv("DATABASE_URL", "postgresql://test:test@localhost/test")
monkeypatch.setenv("SECRET_KEY", "test-secret-key")
monkeypatch.delenv("PRODUCTION_API_KEY", raising=False)
```
## 7. Freezing Time
```python
@pytest.fixture
def frozen_time():
"""Freeze time to a specific moment."""
fixed = datetime(2025, 1, 15, 12, 0, 0, tzinfo=timezone.utc)
with patch("app.services.datetime") as mock_dt:
mock_dt.now.return_value = fixed
mock_dt.utcnow.return_value = fixed
mock_dt.side_effect = lambda *a, **kw: datetime(*a, **kw)
yield fixed
```
Alternative: use `freezegun` library with `@freeze_time("2025-01-15 12:00:00")`.
## 8. Parametrized Fixture
```python
@pytest.fixture(params=["sqlite", "postgresql"])
def db_url(request):
"""Run tests against multiple database backends."""
urls = {
"sqlite": "sqlite:///test.db",
"postgresql": "postgresql://test:test@localhost/test",
}
return urls[request.param]
```
## Fixture Scope Reference
| Scope | Lifetime | Use For |
|-------|----------|---------|
| `function` (default) | Each test | Most fixtures, mutable state |
| `class` | Each test class | Shared setup for a class |
| `module` | Each test file | Expensive setup shared across file |
| `session` | Entire test run | Database engine, heavy resources |
## Tips
- Use `yield` (not `return`) when cleanup is needed after the test.
- Use `autouse=True` sparingly -- only for things every test needs.
- Keep fixtures small and composable -- combine them in tests, not in other fixtures.
- Use `monkeypatch` instead of `unittest.mock.patch` for env vars and attributes when possible.
- Name fixtures after what they provide, not what they do: `db_session` not `setup_database`.
@@ -1,197 +0,0 @@
"""
Starter conftest.py -- common fixtures for pytest.
Usage:
Place this file at the root of your tests/ directory.
pytest automatically discovers conftest.py and makes its fixtures
available to all tests in the same directory and below.
"""
import os
from collections.abc import AsyncGenerator, Generator
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
# ---------------------------------------------------------------------------
# If using FastAPI + httpx:
# from httpx import ASGITransport, AsyncClient
# from app.main import app
# If using SQLAlchemy:
# from sqlalchemy import create_engine
# from sqlalchemy.orm import Session, sessionmaker
# from app.models import Base
# ---------------------------------------------------------------------------
# ==========================================================================
# Environment Variables
# ==========================================================================
@pytest.fixture(autouse=True)
def _test_env(monkeypatch: pytest.MonkeyPatch) -> None:
"""Set safe default environment variables for all tests.
autouse=True ensures this runs for every test automatically.
monkeypatch restores original values after each test.
"""
monkeypatch.setenv("APP_ENV", "test")
monkeypatch.setenv("DEBUG", "true")
monkeypatch.setenv("SECRET_KEY", "test-secret-not-for-production")
monkeypatch.setenv("DATABASE_URL", "sqlite:///test.db")
# Remove any production secrets that should never leak into tests.
monkeypatch.delenv("PRODUCTION_API_KEY", raising=False)
monkeypatch.delenv("AWS_SECRET_ACCESS_KEY", raising=False)
# ==========================================================================
# Temporary Directory
# ==========================================================================
@pytest.fixture
def data_dir(tmp_path: Path) -> Path:
"""Provide a temporary directory with input/output subdirectories."""
(tmp_path / "input").mkdir()
(tmp_path / "output").mkdir()
return tmp_path
@pytest.fixture
def sample_file(tmp_path: Path) -> Path:
"""Create a sample text file for testing file operations."""
f = tmp_path / "sample.txt"
f.write_text("line 1\nline 2\nline 3\n")
return f
# ==========================================================================
# Database Session (SQLAlchemy)
# ==========================================================================
# Uncomment this section if using SQLAlchemy.
# TEST_DATABASE_URL = os.getenv(
# "TEST_DATABASE_URL", "postgresql://test:test@localhost:5432/test_db"
# )
#
#
# @pytest.fixture(scope="session")
# def engine():
# """Create database engine for the entire test session."""
# eng = create_engine(TEST_DATABASE_URL)
# yield eng
# eng.dispose()
#
#
# @pytest.fixture(scope="session")
# def tables(engine):
# """Create tables at start of session, drop at end."""
# Base.metadata.create_all(engine)
# yield
# Base.metadata.drop_all(engine)
#
#
# @pytest.fixture
# def db_session(engine, tables) -> Generator[Session, None, None]:
# """Transactional database session -- rolls back after each test."""
# connection = engine.connect()
# transaction = connection.begin()
# session = sessionmaker(bind=connection)()
#
# yield session
#
# session.close()
# transaction.rollback()
# connection.close()
# ==========================================================================
# HTTP Test Client (FastAPI)
# ==========================================================================
# Uncomment this section if using FastAPI.
# @pytest.fixture
# async def client() -> AsyncGenerator[AsyncClient, None]:
# """Async HTTP client for testing API endpoints."""
# transport = ASGITransport(app=app)
# async with AsyncClient(transport=transport, base_url="http://test") as c:
# yield c
#
#
# @pytest.fixture
# def auth_headers() -> dict[str, str]:
# """Authorization headers with a test JWT token."""
# from app.auth import create_access_token
# token = create_access_token(data={"sub": "test-user", "role": "admin"})
# return {"Authorization": f"Bearer {token}"}
#
#
# @pytest.fixture
# async def auth_client(auth_headers) -> AsyncGenerator[AsyncClient, None]:
# """Authenticated async HTTP client."""
# transport = ASGITransport(app=app)
# async with AsyncClient(
# transport=transport, base_url="http://test", headers=auth_headers
# ) as c:
# yield c
# ==========================================================================
# Mock External Services
# ==========================================================================
@pytest.fixture
def mock_http_client() -> MagicMock:
"""Generic mock HTTP client with default 200/201 responses."""
client = MagicMock()
client.get.return_value = MagicMock(
status_code=200,
json=lambda: {"status": "ok"},
)
client.post.return_value = MagicMock(
status_code=201,
json=lambda: {"id": "new-123"},
)
return client
# @pytest.fixture
# def mock_email_service():
# """Mock email service to prevent real emails in tests."""
# with patch("app.services.email.send_email") as mock_send:
# mock_send.return_value = {"message_id": "test-msg-001"}
# yield mock_send
# ==========================================================================
# Factory Fixtures
# ==========================================================================
# @pytest.fixture
# def make_user(db_session):
# """Factory fixture: creates User instances with defaults."""
# created = []
#
# def _make_user(
# name: str = "Test User",
# email: str | None = None,
# is_active: bool = True,
# ):
# from app.models import User
# if email is None:
# email = f"user-{len(created)}@test.com"
# user = User(name=name, email=email, is_active=is_active)
# db_session.add(user)
# db_session.flush()
# created.append(user)
# return user
#
# return _make_user
+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
@@ -1,8 +1,5 @@
---
name: pytest
description: >
Trigger this skill whenever writing, debugging, or refactoring Python tests, or when pytest fixtures, parametrization, mocking, or coverage are mentioned. Activate for any .py test file, test_* function, conftest.py, pytest.ini, or pyproject.toml [tool.pytest] reference. Also use when the user asks about Python test patterns, test organization, or test-driven development in a Python context.
---
# Testing — pytest Patterns
# pytest
@@ -14,7 +11,7 @@ description: >
## When NOT to Use
- JavaScript or TypeScript testing -- use the `testing/vitest` skill instead
- 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
@@ -683,7 +680,7 @@ def test_create_user(client):
## Related Skills
- `testing/vitest` -- JavaScript/TypeScript testing counterpart
- `languages/python` -- Python language patterns and idioms
- `methodology/test-driven-development` -- TDD workflow for writing tests first
- `devops/github-actions` — Running pytest in CI/CD pipelines
- `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
@@ -1,8 +1,5 @@
---
name: vitest
description: >
Trigger this skill whenever writing, debugging, or refactoring JavaScript or TypeScript tests, or when Vitest mocking, coverage, or configuration are mentioned. Activate for any .test.ts, .test.tsx, .test.js, .spec.ts, .spec.js file, vitest.config.ts reference, or React component testing with Testing Library. Also use when the user asks about JS/TS test patterns, test organization, or vi.mock/vi.fn usage.
---
# Testing — Vitest Patterns
# Vitest
@@ -14,7 +11,7 @@ description: >
## When NOT to Use
- Python testing -- use the `testing/pytest` skill instead
- Python testing -- use the `pytest` skill instead
- Projects that explicitly mandate Jest-only by convention with no Vitest dependency
- Non-JavaScript/TypeScript projects
@@ -838,8 +835,8 @@ When `globals: true` is set in config, you do not need to import `describe`, `it
## Related Skills
- `testing/pytest` -- Python testing counterpart
- `languages/typescript` -- TypeScript language patterns and strict typing
- `frameworks/react` -- React component patterns for component testing
- `methodology/test-driven-development` -- TDD workflow for writing tests first
- `devops/github-actions` — Running vitest in CI/CD pipelines
- `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
@@ -1,242 +0,0 @@
# Vitest Mock Patterns
Catalog of mocking patterns for common testing scenarios.
## 1. Module Mock (Full)
Replace an entire module with mock implementations.
```typescript
import { describe, it, expect, vi } from "vitest";
// Mock the entire module BEFORE importing code that uses it.
vi.mock("@/services/payment", () => ({
chargeCard: vi.fn().mockResolvedValue({
transactionId: "txn-123",
status: "succeeded",
}),
refundCharge: vi.fn().mockResolvedValue({
refundId: "ref-456",
status: "refunded",
}),
}));
import { chargeCard } from "@/services/payment";
import { checkout } from "@/services/checkout";
describe("checkout", () => {
it("should charge the card and return success", async () => {
const result = await checkout({ amount: 42, cardToken: "tok_test" });
expect(chargeCard).toHaveBeenCalledWith({
amount: 42,
token: "tok_test",
});
expect(result.status).toBe("succeeded");
});
});
```
## 2. Partial Module Mock
Mock only specific exports; keep the rest real.
```typescript
import { describe, it, expect, vi } from "vitest";
vi.mock("@/utils/config", async (importOriginal) => {
const actual = await importOriginal<typeof import("@/utils/config")>();
return {
...actual,
// Override only this one export
getFeatureFlag: vi.fn().mockReturnValue(true),
};
});
import { getFeatureFlag, parseConfig } from "@/utils/config";
describe("with feature flag enabled", () => {
it("should use the new algorithm", () => {
// getFeatureFlag is mocked, parseConfig is real
expect(getFeatureFlag("new-algo")).toBe(true);
});
});
```
## 3. Manual Mock Reset / Per-Test Overrides
```typescript
import { describe, it, expect, vi, beforeEach } from "vitest";
import { fetchUser } from "@/api/users";
vi.mock("@/api/users");
// Type the mock for autocomplete
const mockFetchUser = vi.mocked(fetchUser);
beforeEach(() => {
vi.resetAllMocks(); // Clear call history AND implementations
});
describe("user profile", () => {
it("shows user data on success", async () => {
mockFetchUser.mockResolvedValueOnce({ id: "1", name: "Alice" });
// ...test
});
it("shows error on failure", async () => {
mockFetchUser.mockRejectedValueOnce(new Error("Network error"));
// ...test
});
});
```
## 4. API Mock with MSW (Mock Service Worker)
Best for integration tests that should exercise real fetch/axios code.
```typescript
// test/mocks/handlers.ts
import { http, HttpResponse } from "msw";
export const handlers = [
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: "new-1", ...body }, { status: 201 });
}),
];
```
```typescript
// test/mocks/server.ts
import { setupServer } from "msw/node";
import { handlers } from "./handlers";
export const server = setupServer(...handlers);
```
```typescript
// test/setup.ts (referenced in vitest.config.ts setupFiles)
import { afterAll, afterEach, beforeAll } from "vitest";
import { server } from "./mocks/server";
beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
```
```typescript
// Usage in tests -- override handlers per test
import { http, HttpResponse } from "msw";
import { server } from "../mocks/server";
it("handles server error", async () => {
server.use(
http.get("/api/users/:id", () => {
return HttpResponse.json({ error: "Not found" }, { status: 404 });
}),
);
// ...test error handling
});
```
## 5. Timer Mocks
Control `setTimeout`, `setInterval`, `Date.now`.
```typescript
beforeEach(() => vi.useFakeTimers());
afterEach(() => vi.useRealTimers());
it("should call the function after the delay", () => {
const fn = vi.fn();
const debounced = debounce(fn, 300);
debounced();
expect(fn).not.toHaveBeenCalled();
vi.advanceTimersByTime(300);
expect(fn).toHaveBeenCalledOnce();
});
// Fake date: vi.setSystemTime(new Date("2025-01-15T12:00:00Z"))
```
## 6. Spy Patterns
Observe calls without replacing implementation.
```typescript
import { describe, it, expect, vi } from "vitest";
describe("logging", () => {
it("should log errors to console", () => {
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
logError("something went wrong");
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining("something went wrong"),
);
consoleSpy.mockRestore();
});
});
// Spy on object method without changing behavior
it("should call save", () => {
const repo = new UserRepository();
const saveSpy = vi.spyOn(repo, "save");
repo.createUser({ name: "Alice" });
expect(saveSpy).toHaveBeenCalledOnce();
expect(saveSpy).toHaveBeenCalledWith(expect.objectContaining({ name: "Alice" }));
});
```
## 7. Global / Window Mocks
```typescript
// Mock window.location
vi.spyOn(window, "location", "get").mockReturnValue({ ...window.location, pathname: "/dashboard" });
// Mock localStorage
const storage: Record<string, string> = {};
vi.spyOn(Storage.prototype, "getItem").mockImplementation((key) => storage[key] ?? null);
vi.spyOn(Storage.prototype, "setItem").mockImplementation((key, val) => { storage[key] = val; });
// Mock fetch (when not using MSW)
global.fetch = vi.fn().mockResolvedValue({ ok: true, json: () => Promise.resolve({ data: "test" }) });
```
## 8. Class Mock
```typescript
vi.mock("@/services/analytics", () => ({
AnalyticsClient: vi.fn().mockImplementation(() => ({
track: vi.fn(),
identify: vi.fn(),
flush: vi.fn().mockResolvedValue(undefined),
})),
}));
```
## Quick Reference: Mock Functions
| Method | Purpose |
|--------|---------|
| `vi.fn()` | Create a standalone mock function |
| `vi.fn().mockReturnValue(x)` | Always return `x` |
| `vi.fn().mockReturnValueOnce(x)` | Return `x` once, then default |
| `vi.fn().mockResolvedValue(x)` | Return `Promise.resolve(x)` |
| `vi.fn().mockRejectedValue(e)` | Return `Promise.reject(e)` |
| `vi.fn().mockImplementation(fn)` | Use custom implementation |
| `vi.spyOn(obj, "method")` | Spy on existing method |
| `vi.mocked(fn)` | Type helper for mocked function |
| `vi.mock("module")` | Auto-mock all exports |
| `vi.resetAllMocks()` | Reset history and implementations |
| `vi.restoreAllMocks()` | Restore original implementations |
| `vi.clearAllMocks()` | Clear call history only |
@@ -1,100 +0,0 @@
/// <reference types="vitest/config" />
import { defineConfig } from "vitest/config";
import path from "node:path";
export default defineConfig({
// -------------------------------------------------------------------------
// Path aliases -- must match tsconfig.json "paths"
// -------------------------------------------------------------------------
resolve: {
alias: {
"@": path.resolve(__dirname, "src"),
"@test": path.resolve(__dirname, "test"),
},
},
test: {
// -----------------------------------------------------------------------
// Environment
// -----------------------------------------------------------------------
// "node" -- default, for backend / library code
// "jsdom" -- for code that accesses DOM APIs (React, etc.)
// "happy-dom" -- faster jsdom alternative
environment: "jsdom",
// -----------------------------------------------------------------------
// Globals
// -----------------------------------------------------------------------
// Set to true to use describe/it/expect without importing from "vitest".
// Requires adding "vitest/globals" to tsconfig "types".
globals: true,
// -----------------------------------------------------------------------
// Setup files -- run before each test file
// -----------------------------------------------------------------------
setupFiles: [
"./test/setup.ts",
// "./test/mocks/server.ts", // MSW server setup
],
// -----------------------------------------------------------------------
// File patterns
// -----------------------------------------------------------------------
include: [
"src/**/*.{test,spec}.{ts,tsx}",
"test/**/*.{test,spec}.{ts,tsx}",
],
exclude: [
"node_modules",
"dist",
"e2e/**",
],
// -----------------------------------------------------------------------
// Coverage
// -----------------------------------------------------------------------
coverage: {
provider: "v8", // or "istanbul"
reporter: ["text", "text-summary", "lcov", "json"],
reportsDirectory: "./coverage",
include: ["src/**/*.{ts,tsx}"],
exclude: [
"src/**/*.d.ts",
"src/**/*.test.{ts,tsx}",
"src/**/*.spec.{ts,tsx}",
"src/**/index.ts", // barrel files
"src/types/**",
],
// Minimum thresholds -- fail if coverage drops below these.
thresholds: {
statements: 80,
branches: 80,
functions: 80,
lines: 80,
},
},
// -----------------------------------------------------------------------
// Timeouts
// -----------------------------------------------------------------------
testTimeout: 10_000, // 10s per test
hookTimeout: 10_000, // 10s per beforeEach/afterEach
// -----------------------------------------------------------------------
// Reporters
// -----------------------------------------------------------------------
reporters: ["default"],
// For CI, add JUnit output:
// reporters: ["default", "junit"],
// outputFile: { junit: "./junit.xml" },
// -----------------------------------------------------------------------
// Other options
// -----------------------------------------------------------------------
// restoreMocks: true, // Automatically restore mocks after each test
// clearMocks: true, // Clear mock call history after each test
// mockReset: true, // Reset mocks (clear + remove implementations)
},
});