| name | typescript-testing-backend |
| description | This skill provides backend testing rules with Vitest, real DB/API integration patterns, and service-level testing conventions. Automatically loaded when writing backend tests, reviewing test quality, or when "backend test", "service test", "API test", "integration test", "database test", or "backend test coverage" are mentioned. |
TypeScript Testing Rules (Backend)
Test Framework
- Vitest: Primary test runner
- Supertest: For HTTP endpoint testing
- fast-check: For property-based testing
- Test imports:
import { describe, it, expect, beforeEach, vi } from 'vitest'
- Mock creation: Use
vi.mock()
Basic Testing Policy
Quality Requirements
- Coverage: Unit test coverage must be 70% or higher (backend standard)
- Independence: Each test can run independently without depending on other tests
- Reproducibility: Tests are environment-independent and always return the same results
- Readability: Test code maintains the same quality as production code
Coverage Requirements
Mandatory: Unit test coverage must be 70% or higher
Layer-specific targets:
- Services (Business Logic): 80% or higher
- Controllers (API Layer): 70% or higher
- Repositories (Data Access): 60% or higher — focus on custom query methods
- Utils/Helpers: 80% or higher
- Guards/Interceptors/Pipes: 70% or higher
Metrics: Statements, Branches, Functions, Lines
Test Types and Scope
-
Unit Tests
- Verify behavior of individual services, functions, or classes
- Mock all external dependencies (DB, APIs, message queues)
- Most numerous, implemented with fine granularity
- Focus on business logic correctness
-
Integration Tests
- Verify coordination between layers (Controller → Service → Repository)
- Use real database (test instance) or in-memory database
- Use real HTTP through Supertest
- Test API contracts end-to-end within the service
-
Property-Based Tests (fast-check)
- Verify invariants hold across random inputs
- Use for: parsers, validators, serializers, mathematical operations
- Complement example-based tests — do not replace them
Red-Green-Refactor Process (Test-First Development)
Recommended Principle: Always start code changes with tests
Development Steps:
- Red: Write test for expected behavior (it fails)
- Green: Pass test with minimal implementation
- Refactor: Improve code while maintaining passing tests
NG Cases (Test-first not required):
- Pure configuration file changes (nest-cli.json, ormconfig.ts, etc.)
- Documentation-only updates
- Emergency production incident response (post-incident tests mandatory)
Test Design Principles
Test Case Structure
- Tests consist of three stages: "Arrange," "Act," "Assert"
- Clear naming that shows purpose of each test
- One test case verifies only one behavior
Test Data Management
- Manage test data in
__tests__/ directories or co-located with source
- Use factory functions or builder patterns for test data creation
- Always mock sensitive information (API keys, credentials)
- Keep test data minimal — only data directly related to test verification
- Use database fixtures or seeding for integration tests
Mock and Stub Usage Policy
Recommended: Mock external dependencies in unit tests
- Mock database connections, external API clients, message queues
- Mock at the boundary — not internal functions
For integration tests: Use real dependencies
- Real database (test instance or in-memory like SQLite)
- Real HTTP stack through Supertest
- Mock only truly external services (third-party APIs)
Test Failure Response Decision Criteria
Fix tests: Wrong expected values, implementation detail coupling, flaky assertions
Fix implementation: Valid business rules, edge cases, contract violations
When in doubt: Confirm with user
Test Implementation Conventions
Directory Structure
src/
├── users/
│ ├── users.service.ts
│ ├── users.controller.ts
│ ├── __tests__/
│ │ ├── users.service.test.ts
│ │ ├── users.controller.test.ts
│ │ └── users.integration.test.ts
│ └── index.ts
Rationale:
__tests__/ directory convention for backend projects
- Clear separation between unit and integration tests
- Easy to find and maintain tests alongside implementation
Naming Conventions
- Test files:
{module}.{layer}.test.ts (e.g., users.service.test.ts)
- Integration test files:
{feature}.integration.test.ts
- Test suites: Names describing target module or feature
- Test cases: Names describing expected behavior from caller perspective
Test Code Quality Rules
Recommended: Keep all tests always active
- Fix problematic tests and activate them
Avoid: test.skip() or commenting out
- Creates test gaps and incomplete quality checks
- Solution: Completely delete unnecessary tests
Service Testing Patterns
Unit Testing Services
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { UserService } from '../users.service'
describe('UserService', () => {
let service: UserService
let mockRepository: { findOne: ReturnType<typeof vi.fn>; save: ReturnType<typeof vi.fn> }
beforeEach(() => {
mockRepository = {
findOne: vi.fn(),
save: vi.fn(),
}
service = new UserService(mockRepository as any)
})
it('should return user when found', async () => {
const expectedUser = { id: '1', name: 'John', email: 'john@example.com' }
mockRepository.findOne.mockResolvedValue(expectedUser)
const result = await service.findById('1')
expect(result).toEqual(expectedUser)
expect(mockRepository.findOne).toHaveBeenCalledWith({ where: { id: '1' } })
})
it('should throw NotFoundError when user not found', async () => {
mockRepository.findOne.mockResolvedValue(null)
await expect(service.findById('999')).rejects.toThrow(NotFoundError)
})
})
Integration Testing with Supertest
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import request from 'supertest'
import { createApp } from '../app'
describe('POST /api/users', () => {
let app: Express
beforeAll(async () => {
app = await createApp({ database: 'test' })
})
afterAll(async () => {
await app.close()
})
it('should create user and return 201', async () => {
const response = await request(app)
.post('/api/users')
.send({ name: 'Jane', email: 'jane@example.com' })
.expect(201)
expect(response.body).toMatchObject({
name: 'Jane',
email: 'jane@example.com',
})
expect(response.body.id).toBeDefined()
})
it('should return 400 for invalid email', async () => {
await request(app)
.post('/api/users')
.send({ name: 'Jane', email: 'not-an-email' })
.expect(400)
})
})
Property-Based Testing with fast-check
import { describe, it, expect } from 'vitest'
import fc from 'fast-check'
import { parseUserId } from '../utils/parse-user-id'
describe('parseUserId', () => {
it('should round-trip: format(parse(id)) === id for valid UUIDs', () => {
fc.assert(
fc.property(fc.uuid(), (uuid) => {
const parsed = parseUserId(uuid)
expect(parsed.toString()).toBe(uuid)
})
)
})
it('should reject non-UUID strings', () => {
fc.assert(
fc.property(
fc.string().filter((s) => !isValidUuid(s)),
(invalidId) => {
expect(() => parseUserId(invalidId)).toThrow()
}
)
)
})
})
Test Quality Criteria
Literal Expected Values
Use hardcoded literal values for assertions:
expect(calculatePrice(100, 0.1)).toBe(110)
expect(formatDate(new Date('2025-01-15'))).toBe('2025-01-15')
expect(user.role).toBe('admin')
Result-Based Verification
Verify final results and outcomes:
expect(mockRepository.save).toHaveBeenCalledWith({ name: 'test', email: 'test@example.com' })
expect(result).toEqual({ id: '1', status: 'created' })
expect(response.statusCode).toBe(201)
Meaningful Assertions
Every test must include at least one expect() that validates observable behavior.
Appropriate Mock Scope
Mock only direct external I/O dependencies. Internal utilities use real implementations:
vi.mock('../repositories/user.repository')
vi.mock('../clients/email.client')
Database Testing Patterns
Test Database Setup
- Use separate test database instance or in-memory alternative
- Run migrations before test suite
- Clean data between tests (truncate or transaction rollback)
- Seed with minimal required data per test
Transaction Rollback Pattern
beforeEach(async () => {
await dataSource.query('BEGIN')
})
afterEach(async () => {
await dataSource.query('ROLLBACK')
})
Avoid Shared State
- Each test creates its own data
- Never depend on data created by another test
- Use unique identifiers to prevent collision in parallel runs