一键导入
tdd-domain-layer
Strict TDD for Domain and Application layers in Clean Architecture TypeScript. Red-Green-Refactor with Vitest. Mocks ports, never hits a database.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Strict TDD for Domain and Application layers in Clean Architecture TypeScript. Red-Green-Refactor with Vitest. Mocks ports, never hits a database.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
| name | tdd-domain-layer |
| description | Strict TDD for Domain and Application layers in Clean Architecture TypeScript. Red-Green-Refactor with Vitest. Mocks ports, never hits a database. |
Strict Test-Driven Development practitioner for Clean Architecture TypeScript using Vitest (or Jest). Tests for Domain and Application layers must never connect to a database, external API, or Express server.
@prisma/client, express, or socket.io.npm install -D vitest @vitest/coverage-v8
// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'node',
coverage: {
provider: 'v8',
include: ['src/domain/**', 'src/application/**'],
thresholds: { lines: 80, functions: 80 },
},
},
});
Focus: State transitions, invariant enforcement, Domain Event generation.
// src/domain/value-objects/__tests__/email.vo.spec.ts
import { describe, it, expect } from 'vitest';
import { Email } from '../email.vo';
import { InvalidEmailException } from '../../exceptions/invalid-email.exception';
describe('Email Value Object', () => {
it('creates a valid email in lowercase', () => {
const email = Email.create('Alice@Example.COM');
expect(email.toString()).toBe('alice@example.com');
});
it('trims whitespace', () => {
const email = Email.create(' bob@example.com ');
expect(email.toString()).toBe('bob@example.com');
});
it('throws InvalidEmailException for an address without @', () => {
expect(() => Email.create('notanemail')).toThrow(InvalidEmailException);
});
it('considers two emails with same value as equal', () => {
const a = Email.create('alice@example.com');
const b = Email.create('alice@example.com');
expect(a.equals(b)).toBe(true);
});
});
// src/domain/entities/__tests__/user.entity.spec.ts
import { describe, it, expect } from 'vitest';
import { User } from '../user.entity';
import { Email } from '../../value-objects/email.vo';
import { UserId } from '../../value-objects/user-id.vo';
import { UserRegisteredEvent } from '../../events/user-registered.event';
import { WeakPasswordException } from '../../exceptions/weak-password.exception';
describe('User Entity', () => {
const id = UserId.generate();
const email = Email.create('alice@example.com');
describe('User.register()', () => {
it('creates a User with the given email', () => {
const user = User.register(id, email, 'hashedPw123');
expect(user.getEmail().toString()).toBe('alice@example.com');
});
it('emits a UserRegisteredEvent', () => {
const user = User.register(id, email, 'hashedPw123');
const events = user.pullDomainEvents();
expect(events).toHaveLength(1);
expect(events[0]).toBeInstanceOf(UserRegisteredEvent);
});
it('clears domain events after pulling them', () => {
const user = User.register(id, email, 'hashedPw123');
user.pullDomainEvents();
expect(user.pullDomainEvents()).toHaveLength(0);
});
});
describe('user.changePassword()', () => {
it('updates the password hash', () => {
const user = User.register(id, email, 'oldHash');
user.changePassword('newStrongHash');
// verify via a query method or reconstitution if needed
});
it('throws WeakPasswordException for an empty string', () => {
const user = User.register(id, email, 'oldHash');
expect(() => user.changePassword('')).toThrow(WeakPasswordException);
});
});
});
Focus: Orchestration. Does the Use Case fetch data, call the right Entity method, save it, and publish events?
// src/application/use-cases/__tests__/register-user.use-case.spec.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { RegisterUserUseCase } from '../register-user.use-case';
import { IUserRepository } from '../../ports/user-repository.port';
import { IRealTimePublisher } from '../../ports/realtime-publisher.port';
import { UserAlreadyExistsException } from '../../../domain/exceptions/user-already-exists.exception';
const makeUserRepo = (): IUserRepository => ({
findById: vi.fn(),
findByEmail: vi.fn().mockResolvedValue(null), // default: user does not exist
save: vi.fn().mockResolvedValue(undefined),
});
const makePublisher = (): IRealTimePublisher => ({
publish: vi.fn(),
});
describe('RegisterUserUseCase', () => {
let userRepo: IUserRepository;
let publisher: IRealTimePublisher;
let useCase: RegisterUserUseCase;
beforeEach(() => {
userRepo = makeUserRepo();
publisher = makePublisher();
useCase = new RegisterUserUseCase(userRepo, publisher);
});
it('saves the new user to the repository', async () => {
await useCase.execute({ email: 'alice@example.com', password: 'StrongPass1!' });
expect(userRepo.save).toHaveBeenCalledOnce();
});
it('publishes a user.registered event', async () => {
await useCase.execute({ email: 'alice@example.com', password: 'StrongPass1!' });
expect(publisher.publish).toHaveBeenCalledWith('user.registered', expect.objectContaining({
email: expect.objectContaining({}),
}));
});
it('throws UserAlreadyExistsException when email is taken', async () => {
vi.mocked(userRepo.findByEmail).mockResolvedValue({ getId: vi.fn() } as any);
await expect(
useCase.execute({ email: 'existing@example.com', password: 'StrongPass1!' }),
).rejects.toThrow(UserAlreadyExistsException);
expect(userRepo.save).not.toHaveBeenCalled();
});
it('does NOT call the publisher if saving the user fails', async () => {
vi.mocked(userRepo.save).mockRejectedValue(new Error('DB error'));
await expect(
useCase.execute({ email: 'alice@example.com', password: 'StrongPass1!' }),
).rejects.toThrow('DB error');
expect(publisher.publish).not.toHaveBeenCalled();
});
});
1. Write .spec.ts — describe expected behaviour, all tests FAIL (red)
2. Run: npx vitest run
3. Write minimum implementation to make tests pass (green)
4. Run: npx vitest run ← must all pass
5. Refactor implementation (no behaviour change)
6. Run: npx vitest run ← still all passing
// package.json
{
"scripts": {
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage"
}
}
| ❌ Never Do | ✅ Instead |
|---|---|
import { PrismaClient } from '@prisma/client' in a .spec.ts | Mock the IRepository interface with vi.fn() |
import supertest from 'supertest' in a Domain/Use Case test | Use supertest only in E2E / integration tests |
jest.mock('../user.entity') | Never mock Domain Entities — test them directly |
Testing implementation details (user._passwordHash) | Test behaviour via public methods |
| Writing implementation before the test | Write the failing test first, always |
| Single giant test covering everything | One it() per behaviour, descriptive names |
Review TypeScript DDD clean architecture code for correctness bugs, layer violations, and simplification opportunities. Trigger when the user says "review this", "check this code", "code review", "what's wrong with this", "is this correct DDD?", "does this follow clean architecture?", or when changes touch domain, application, or infrastructure layers.
Security review for TypeScript DDD code — OWASP-mapped checks for injection, auth/authz, data exposure, and misconfiguration. Trigger when the user says "security review", "check for vulnerabilities", "is this secure?", "review auth code", or when changes touch authentication, authorization, input handling, external APIs, file uploads, or user-controlled data.
Review and simplify TypeScript DDD code — remove unnecessary complexity, eliminate duplication, reduce premature abstraction. Trigger when the user says "simplify this", "too much boilerplate", "over-engineered", "clean this up", "refactor this", or when code has grown too abstract or introduces patterns before they prove value.
Design and implement the CQRS (Command Query Responsibility Segregation) pattern in a TypeScript DDD clean architecture project. Trigger when the user says "implement CQRS", "add a command", "add a query", "create a use case", "add a command handler", "build the application layer", "set up the command bus", or when the user needs to add a new feature and is asking how to wire up the application layer. Also trigger when distinguishing between write operations (commands) and read operations (queries) in any context.
Design and implement the Repository pattern in a TypeScript DDD clean architecture project — define repository interfaces in the domain layer and implementations in the infrastructure layer. Trigger when the user says "create a repository", "implement persistence", "add database access", "wire up TypeORM/Prisma/Drizzle", "implement the repository interface", "add Unit of Work", "how do I persist this aggregate", or when connecting domain aggregates to any data store. Also trigger when reviewing persistence code that may be violating the dependency rule.
Design and implement CI/CD pipelines for a TypeScript DDD clean architecture project — GitHub Actions, GitLab CI, Docker builds, environment promotion, and secrets management. Trigger when the user says "set up CI", "add a pipeline", "automate tests", "write a GitHub Actions workflow", "configure deployment", "add Docker support", "set up CD", "automate the build", or when the project needs automated quality gates before merge. Also trigger when the user asks about environment promotion (dev → staging → prod) or secrets management strategy.