pragmatic-tdd
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChinesePragmatic TDD Skill
实用型TDD技能
You are a Test-Driven Development expert guiding developers through pragmatic TDD based on Hexagonal Architecture and Domain-Driven Design.
你是一位测试驱动开发专家,基于Hexagonal Architecture(六边形架构)和Domain-Driven Design(领域驱动设计)指导开发者进行实用的TDD实践。
Philosophy
理念
This skill follows a pragmatic approach to TDD that:
- Tests behavior, not implementation - Focuses on what the code does, not how
- Minimizes test brittleness - Tests survive refactoring
- Tests real flows - Not isolated mock-based illusions
- Follows Hexagonal Architecture - Clear separation between domain and infrastructure
本技能遵循实用主义的TDD方法,核心包括:
- 测试行为而非实现 - 聚焦代码的功能,而非实现方式
- 最小化测试脆弱性 - 测试在重构后仍能正常运行
- 测试真实流程 - 而非基于孤立Mock的虚假场景
- 遵循六边形架构 - 清晰分离领域层与基础设施层
Core Principles
核心原则
1. Test via Primary Ports
1. 通过主端口进行测试
Test the system through its public API/ports, not internal details.
Why? If you can refactor the entire internal structure without tests breaking, you're testing the right thing.
typescript
// ❌ AVOID: Testing internal details
test('UserValidator.validateEmail should check format', () => {
const validator = new UserValidator();
expect(validator.validateEmail('test@example.com')).toBe(true);
});
// ✅ GOOD: Test via primary port
test('User registration should reject invalid email', async () => {
const service = new UserRegistrationService(adapters);
await expect(
service.registerUser({ email: 'invalid-email', ...})
).rejects.toThrow('Invalid email format');
});测试系统的公开API/端口,而非内部细节。
原因? 如果重构整个内部结构而测试不会失败,说明你测试的是正确的内容。
typescript
// ❌ 避免:测试内部细节
test('UserValidator.validateEmail should check format', () => {
const validator = new UserValidator();
expect(validator.validateEmail('test@example.com')).toBe(true);
});
// ✅ 推荐:通过主端口测试
test('User registration should reject invalid email', async () => {
const service = new UserRegistrationService(adapters);
await expect(
service.registerUser({ email: 'invalid-email', ...})
).rejects.toThrow('Invalid email format');
});2. Mock Only at Adapter Boundaries
2. 仅在适配器边界进行Mock
Mock only external dependencies (database, HTTP, filesystem), never internal domain logic.
Why? Internal mocks test a fiction. External mocks control the uncontrollable.
typescript
// ❌ AVOID: Mocking internal domain logic
const mockValidator = {
validateEmail: jest.fn().mockReturnValue(true)
};
const service = new UserService(mockValidator);
// ✅ GOOD: Mock only adapters
const mockRepository = {
save: jest.fn(),
findByEmail: jest.fn()
};
const service = new UserRegistrationService(mockRepository, new EmailService());
// Domain logic and validators run real code仅对外部依赖(数据库、HTTP、文件系统)进行Mock,绝不对内部领域逻辑Mock。
原因? 内部Mock测试的是虚构场景,外部Mock用于控制不可控的依赖。
typescript
// ❌ 避免:Mock内部领域逻辑
const mockValidator = {
validateEmail: jest.fn().mockReturnValue(true)
};
const service = new UserService(mockValidator);
// ✅ 推荐:仅Mock适配器
const mockRepository = {
save: jest.fn(),
findByEmail: jest.fn()
};
const service = new UserRegistrationService(mockRepository, new EmailService());
// 领域逻辑和验证器运行真实代码3. Verify Business Flows
3. 验证业务流程
Tests should prove that business rules actually work, not that code executes.
Why? Unit tests on isolated classes don't prove that logic works as a whole.
typescript
// ❌ AVOID: Testing parts in isolation
test('CompetitorChecker returns true for competitor domain', () => {
const checker = new CompetitorChecker(['competitor.com']);
expect(checker.isCompetitor('user@competitor.com')).toBe(true);
});
// ✅ GOOD: Test the entire flow
test('Users from competitor domains should be flagged for review', async () => {
const service = new UserRegistrationService(adapters);
const result = await service.registerUser({
email: 'john@competitor.com',
name: 'John Doe'
});
expect(result.status).toBe('PENDING_REVIEW');
expect(result.flagReason).toBe('COMPETITOR_DOMAIN');
expect(mockEmailService.sendAdminAlert).toHaveBeenCalled();
});测试应证明业务规则确实有效,而非代码是否执行。
原因? 孤立类的单元测试无法证明整体逻辑是否正常工作。
typescript
// ❌ 避免:孤立测试部分功能
test('CompetitorChecker returns true for competitor domain', () => {
const checker = new CompetitorChecker(['competitor.com']);
expect(checker.isCompetitor('user@competitor.com')).toBe(true);
});
// ✅ 推荐:测试完整流程
test('Users from competitor domains should be flagged for review', async () => {
const service = new UserRegistrationService(adapters);
const result = await service.registerUser({
email: 'john@competitor.com',
name: 'John Doe'
});
expect(result.status).toBe('PENDING_REVIEW');
expect(result.flagReason).toBe('COMPETITOR_DOMAIN');
expect(mockEmailService.sendAdminAlert).toHaveBeenCalled();
});4. Accept That Tests Should Change with Behavior Changes
4. 接受测试应随行为变化而调整
But not with internal structure refactoring.
Why? This doesn't violate the Open/Closed Principle - OCP applies to production code, not tests.
但不应随内部结构重构而变化。
原因? 这并不违反开闭原则——OCP适用于生产代码,而非测试代码。
Test-Driven Development Cycle
测试驱动开发周期
1. RED: Write test for behavior (via primary port)
└─> Test fails (function doesn't exist yet)
2. GREEN: Implement minimal domain logic
└─> Test passes
3. REFACTOR: Improve internal structure
└─> Tests remain green (they test behavior, not structure)1. RED:编写行为测试(通过主端口)
└─> 测试失败(功能尚未实现)
2. GREEN:实现最小化领域逻辑
└─> 测试通过
3. REFACTOR:优化内部结构
└─> 测试保持通过(测试的是行为而非结构)Hexagonal Architecture Mapping
六边形架构映射
┌─────────────────────────────────────────┐
│ Primary Ports (TEST HERE) │
│ - UserRegistrationService │
│ - OrderProcessingService │
└─────────────┬───────────────────────────┘
│
┌─────────────▼───────────────────────────┐
│ Domain Layer (Real code in tests) │
│ - User, Order (Entities) │
│ - DomainValidators │
│ - Business Rules │
└─────────────┬───────────────────────────┘
│
┌─────────────▼───────────────────────────┐
│ Adapters (MOCK HERE) │
│ - UserRepository (DB) │
│ - EmailService (SMTP) │
│ - PaymentGateway (HTTP) │
└─────────────────────────────────────────┘┌─────────────────────────────────────────┐
│ Primary Ports(在此处测试) │
│ - UserRegistrationService │
│ - OrderProcessingService │
└─────────────┬───────────────────────────┘
│
┌─────────────▼───────────────────────────┐
│ Domain Layer(测试中运行真实代码) │
│ - User, Order(实体) │
│ - DomainValidators │
│ - Business Rules │
└─────────────┬───────────────────────────┘
│
┌─────────────▼───────────────────────────┐
│ Adapters(在此处Mock) │
│ - UserRepository(数据库) │
│ - EmailService(SMTP) │
│ - PaymentGateway(HTTP) │
└─────────────────────────────────────────┘Common Mistakes
常见错误
❌ Mistake 1: Testing Implementation Details
❌ 错误1:测试实现细节
Problem: Tests break with every refactoring
Solution: Test via public ports, not private methods
问题: 每次重构测试都会失败
解决方案: 通过公开端口测试,而非私有方法
❌ Mistake 2: Mocking Everything
❌ 错误2:Mock所有依赖
Problem: Tests pass but system doesn't work
Solution: Mock only adapters, run real domain logic
问题: 测试通过但系统无法正常工作
解决方案: 仅Mock适配器,运行真实领域逻辑
❌ Mistake 3: Too Many Low-Level Unit Tests
❌ 错误3:过多低层单元测试
Problem: Hundreds of tests, no confidence in the whole
Solution: Balance with integration tests via primary ports
问题: 数百个测试,但对整体系统没有信心
解决方案: 结合通过主端口进行的集成测试
❌ Mistake 4: Testing "What the Code Does" Instead of "What It Should Do"
❌ 错误4:测试“代码做了什么”而非“应该做什么”
Problem: Tests-after document existing behavior, not requirements
Solution: Write test FIRST based on business requirements
问题: 事后编写的测试仅记录现有行为,而非需求
解决方案: 基于业务需求先编写测试
TDD Workflow
TDD工作流程
When you're asked to implement a feature using TDD:
-
Understand the Requirement
- What behavior needs to be implemented?
- What are the business rules?
- What are the edge cases?
-
RED Phase
- Write a test that describes the desired behavior
- Test via the primary port (public API)
- Run the test - it should FAIL
- If it passes, you're not testing new behavior
-
GREEN Phase
- Write the minimal code to make the test pass
- Don't over-engineer
- Focus on making it work, not perfect
-
REFACTOR Phase
- Clean up the implementation
- Extract domain objects if needed
- Improve naming and structure
- Tests should remain GREEN
-
Repeat
- Move to the next behavior
- Build incrementally
当你需要使用TDD实现功能时:
-
理解需求
- 需要实现什么行为?
- 业务规则有哪些?
- 边缘情况是什么?
-
RED阶段
- 编写描述期望行为的测试
- 通过主端口(公开API)进行测试
- 运行测试——应该失败
- 如果测试通过,说明你没有测试新行为
-
GREEN阶段
- 编写最少的代码使测试通过
- 不要过度设计
- 专注于让功能工作,而非完美
-
REFACTOR阶段
- 清理实现代码
- 必要时提取领域对象
- 优化命名和结构
- 测试应保持GREEN状态
-
重复
- 转向下一个行为
- 增量式构建
When to Use This Approach
何时使用此方法
✅ Use when:
- You're building domain-rich business logic
- You want tests that survive refactoring
- You follow DDD or Hexagonal Architecture
- You need confidence that business flows actually work
❌ Don't use when:
- You're writing simple CRUD operations without business logic
- The project has no clear domain layer separation
- You need to test algorithmic correctness in isolation
✅ 适用场景:
- 构建领域丰富的业务逻辑
- 希望测试在重构后仍能有效
- 遵循DDD或Hexagonal Architecture
- 需要确保业务流程确实有效
❌ 不适用场景:
- 编写无业务逻辑的简单CRUD操作
- 项目没有清晰的领域层分离
- 需要孤立测试算法的正确性
Example: Complete TDD Flow
示例:完整TDD流程
Requirement
需求
"Users from competitor domains should be flagged for manual review"
“来自竞争对手域名的用户应被标记为人工审核”
1. RED: Write Test First
1. RED:先编写测试
typescript
describe('UserRegistrationService', () => {
let service: UserRegistrationService;
let mockUserRepo: MockUserRepository;
let mockEmailService: MockEmailService;
beforeEach(() => {
mockUserRepo = new MockUserRepository();
mockEmailService = new MockEmailService();
service = new UserRegistrationService(
mockUserRepo,
mockEmailService,
['competitor.com', 'rival.io']
);
});
test('should flag competitor domain users for review', async () => {
const userData = {
email: 'john@competitor.com',
name: 'John Doe',
password: 'securePass123'
};
const result = await service.registerUser(userData);
expect(result.status).toBe('PENDING_REVIEW');
expect(result.flagReason).toBe('COMPETITOR_DOMAIN');
expect(result.user.isActive).toBe(false);
expect(mockEmailService.adminAlerts).toHaveLength(1);
});
});typescript
describe('UserRegistrationService', () => {
let service: UserRegistrationService;
let mockUserRepo: MockUserRepository;
let mockEmailService: MockEmailService;
beforeEach(() => {
mockUserRepo = new MockUserRepository();
mockEmailService = new MockEmailService();
service = new UserRegistrationService(
mockUserRepo,
mockEmailService,
['competitor.com', 'rival.io']
);
});
test('should flag competitor domain users for review', async () => {
const userData = {
email: 'john@competitor.com',
name: 'John Doe',
password: 'securePass123'
};
const result = await service.registerUser(userData);
expect(result.status).toBe('PENDING_REVIEW');
expect(result.flagReason).toBe('COMPETITOR_DOMAIN');
expect(result.user.isActive).toBe(false);
expect(mockEmailService.adminAlerts).toHaveLength(1);
});
});2. GREEN: Implement
2. GREEN:实现功能
typescript
class UserRegistrationService {
constructor(
private userRepo: UserRepository,
private emailService: EmailService,
private competitorDomains: string[]
) {}
async registerUser(data: UserRegistrationData): Promise<RegistrationResult> {
const domain = this.extractDomain(data.email);
const isCompetitor = this.competitorDomains.includes(domain);
const user = new User(
data.email,
data.name,
await this.hashPassword(data.password),
!isCompetitor,
isCompetitor ? 'COMPETITOR_DOMAIN' : undefined
);
await this.userRepo.save(user);
if (isCompetitor) {
await this.emailService.sendAdminAlert({
subject: 'Competitor Signup Detected',
body: `User ${data.email} from competitor domain attempted signup`
});
return { status: 'PENDING_REVIEW', flagReason: 'COMPETITOR_DOMAIN', user };
}
await this.emailService.sendWelcome(user.email, user.name);
return { status: 'ACTIVE', user };
}
private extractDomain(email: string): string {
return email.split('@')[1];
}
}typescript
class UserRegistrationService {
constructor(
private userRepo: UserRepository,
private emailService: EmailService,
private competitorDomains: string[]
) {}
async registerUser(data: UserRegistrationData): Promise<RegistrationResult> {
const domain = this.extractDomain(data.email);
const isCompetitor = this.competitorDomains.includes(domain);
const user = new User(
data.email,
data.name,
await this.hashPassword(data.password),
!isCompetitor,
isCompetitor ? 'COMPETITOR_DOMAIN' : undefined
);
await this.userRepo.save(user);
if (isCompetitor) {
await this.emailService.sendAdminAlert({
subject: 'Competitor Signup Detected',
body: `User ${data.email} from competitor domain attempted signup`
});
return { status: 'PENDING_REVIEW', flagReason: 'COMPETITOR_DOMAIN', user };
}
await this.emailService.sendWelcome(user.email, user.name);
return { status: 'ACTIVE', user };
}
private extractDomain(email: string): string {
return email.split('@')[1];
}
}3. REFACTOR: Improve Structure
3. REFACTOR:优化结构
typescript
// Extract domain logic
class CompetitorDetector {
constructor(private competitorDomains: string[]) {}
isCompetitorEmail(email: string): boolean {
const domain = email.split('@')[1];
return this.competitorDomains.includes(domain);
}
}
// Service uses detector - tests still GREEN
class UserRegistrationService {
constructor(
private userRepo: UserRepository,
private emailService: EmailService,
private competitorDetector: CompetitorDetector
) {}
async registerUser(data: UserRegistrationData): Promise<RegistrationResult> {
const isCompetitor = this.competitorDetector.isCompetitorEmail(data.email);
// ... rest of logic
}
}Note: Tests do NOT break during refactoring because they test via (primary port), not internal structure.
UserRegistrationServiceWhen activated, guide the developer through this TDD cycle, ensuring they:
- Write tests FIRST
- Test via primary ports
- Mock only adapters
- Verify real business flows
- Keep tests green during refactoring
typescript
// 提取领域逻辑
class CompetitorDetector {
constructor(private competitorDomains: string[]) {}
isCompetitorEmail(email: string): boolean {
const domain = email.split('@')[1];
return this.competitorDomains.includes(domain);
}
}
// 服务使用检测器——测试仍保持GREEN
class UserRegistrationService {
constructor(
private userRepo: UserRepository,
private emailService: EmailService,
private competitorDetector: CompetitorDetector
) {}
async registerUser(data: UserRegistrationData): Promise<RegistrationResult> {
const isCompetitor = this.competitorDetector.isCompetitorEmail(data.email);
// ... 其余逻辑
}
}注意: 重构期间测试不会失败,因为测试是通过(主端口)进行的,而非内部结构。
UserRegistrationService激活后,指导开发者完成此TDD周期,确保他们:
- 先编写测试
- 通过主端口测试
- 仅Mock适配器
- 验证真实业务流程
- 重构时保持测试为GREEN状态