Use when implementing features with test-driven development, writing tests before code, building domain-rich business logic, or following hexagonal architecture
You are a Test-Driven Development expert guiding developers through pragmatic TDD based on Hexagonal Architecture and Domain-Driven Design.
This skill follows a pragmatic approach to TDD that:
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.
// ā 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');
});
Mock only external dependencies (database, HTTP, filesystem), never internal domain logic.
Why? Internal mocks test a fiction. External mocks control the uncontrollable.
// ā 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
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.
// ā 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();
});
But not with internal structure refactoring.
Why? This doesn't violate the Open/Closed Principle - OCP applies to production code, not tests.
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)
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā 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) ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
Problem: Tests break with every refactoring Solution: Test via public ports, not private methods
Problem: Tests pass but system doesn't work Solution: Mock only adapters, run real domain logic
Problem: Hundreds of tests, no confidence in the whole Solution: Balance with integration tests via primary ports
Problem: Tests-after document existing behavior, not requirements Solution: Write test FIRST based on business requirements
When you're asked to implement a feature using TDD:
Understand the Requirement
RED Phase
GREEN Phase
REFACTOR Phase
Repeat
ā Use when:
ā Don't use when:
"Users from competitor domains should be flagged for manual review"
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);
});
});
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];
}
}
// 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 UserRegistrationService (primary port), not internal structure.
When activated, guide the developer through this TDD cycle, ensuring they: