102 lines
3.5 KiB
Markdown
102 lines
3.5 KiB
Markdown
# Testing Policy
|
|
|
|
Every behavior change requires a corresponding test, and every bug fix requires a regression test.
|
|
|
|
## Principles
|
|
|
|
| Principle | Criteria |
|
|
|-----------|----------|
|
|
| Given-When-Then | Structure tests in 3 phases |
|
|
| One test, one concept | Do not mix multiple concerns in a single test |
|
|
| Test behavior | Test behavior, not implementation details |
|
|
| Independence | Do not depend on other tests or execution order |
|
|
| Type safety | Code must pass the build (type check) |
|
|
| Reproducibility | Do not depend on time or randomness. Same result every run |
|
|
|
|
## Coverage Criteria
|
|
|
|
| Target | Criteria |
|
|
|--------|----------|
|
|
| New behavior | Test required. REJECT if missing |
|
|
| Bug fix | Regression test required. REJECT if missing |
|
|
| Behavior change | Test update required. REJECT if missing |
|
|
| Build (type check) | Build must succeed. REJECT if it fails |
|
|
| Edge cases / boundary values | Test recommended (Warning) |
|
|
|
|
## Test Priority
|
|
|
|
| Priority | Target |
|
|
|----------|--------|
|
|
| High | Business logic, state transitions |
|
|
| Medium | Edge cases, error handling |
|
|
| Low | Simple CRUD, UI appearance |
|
|
|
|
## Test Structure: Given-When-Then
|
|
|
|
```typescript
|
|
test('should return NotFound error when user does not exist', async () => {
|
|
// Given: A non-existent user ID
|
|
const nonExistentId = 'non-existent-id'
|
|
|
|
// When: Attempt to fetch the user
|
|
const result = await getUser(nonExistentId)
|
|
|
|
// Then: NotFound error is returned
|
|
expect(result.error).toBe('NOT_FOUND')
|
|
})
|
|
```
|
|
|
|
## Test Quality
|
|
|
|
| Aspect | Good | Bad |
|
|
|--------|------|-----|
|
|
| Independence | No dependency on other tests | Depends on execution order |
|
|
| Reproducibility | Same result every time | Depends on time or randomness |
|
|
| Clarity | Failure cause is obvious | Failure cause is unclear |
|
|
| Focus | One test, one concept | Multiple concerns mixed |
|
|
|
|
### Naming
|
|
|
|
Test names describe expected behavior. Use the `should {expected behavior} when {condition}` pattern.
|
|
|
|
### Structure
|
|
|
|
- Arrange-Act-Assert pattern (equivalent to Given-When-Then)
|
|
- Avoid magic numbers and magic strings
|
|
|
|
## Test Strategy
|
|
|
|
- Prefer unit tests for logic, integration tests for boundaries
|
|
- Do not overuse E2E tests for what unit tests can cover
|
|
- If new logic only has E2E tests, propose adding unit tests
|
|
|
|
### When Integration Tests Are Required
|
|
|
|
Verify data flow coupling that unit tests alone cannot cover.
|
|
|
|
| Condition | Verdict |
|
|
|-----------|---------|
|
|
| Data flow crossing 3+ modules | Integration test required |
|
|
| New status/state merging into an existing workflow | Integration test for the full transition flow required |
|
|
| New option propagating through a call chain to the endpoint | End-to-end chain coupling test required |
|
|
| All module-level unit tests pass | Unit tests alone are sufficient (when none of the above apply) |
|
|
|
|
## Test Environment Isolation
|
|
|
|
Tie test infrastructure configuration to test scenario parameters. Hardcoded assumptions break under different scenarios.
|
|
|
|
| Principle | Criteria |
|
|
|-----------|----------|
|
|
| Parameter-driven | Generate fixtures and configuration based on test input parameters |
|
|
| No implicit assumptions | Do not depend on a specific environment (e.g., user's personal settings) |
|
|
| Consistency | Related values within test configuration must not contradict each other |
|
|
|
|
```typescript
|
|
// ❌ Hardcoded assumptions — breaks when testing with a different backend
|
|
writeConfig({ backend: 'postgres', connectionPool: 10 })
|
|
|
|
// ✅ Parameter-driven
|
|
const backend = process.env.TEST_BACKEND ?? 'postgres'
|
|
writeConfig({ backend, connectionPool: backend === 'sqlite' ? 1 : 10 })
|
|
```
|