Tdd domain layer
Skill Methasit-Pun/ts-ddd-clean-architecture/skills/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.From its SKILL.md
npx -y skills add Methasit-Pun/ts-ddd-clean-architecture --skill tdd-domain-layerAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
7.9 KB, ~1.9k tokens by cl100k_base, as published. Nobody here has run it
TDD Domain & Application Expert
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.
When to Activate
- Adding a new business rule to an Entity or Aggregate
- Building a new Use Case (command or query)
- Fixing a bug in the Domain or Application layer
- Creating a new Value Object with validation rules
- Ensuring Domain Events are raised correctly
- Verifying that a Use Case calls the right repository methods
- Reviewing tests that are hitting the database (violation — fix them)
Core Principles
- Red-Green-Refactor — Write the failing test FIRST. Then write the minimum implementation to make it pass. Then refactor.
- Total Isolation — Domain and Application tests must never import
@prisma/client,express, orsocket.io. - Mock Ports, Not Domain — In Use Case tests, mock repository and publisher interfaces. Never mock Domain Entities.
Project Setup
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 },
},
},
});
Domain Layer Tests — Entities & Value Objects
Focus: State transitions, invariant enforcement, Domain Event generation.
Value Object Test
// 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('[email protected]');
expect(email.toString()).toBe('[email protected]');
});
it('trims whitespace', () => {
const email = Email.create(' [email protected] ');
expect(email.toString()).toBe('[email protected]');
});
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('[email protected]');
const b = Email.create('[email protected]');
expect(a.equals(b)).toBe(true);
});
});
Entity / Aggregate Test
// 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('[email protected]');
describe('User.register()', () => {
it('creates a User with the given email', () => {
const user = User.register(id, email, 'hashedPw123');
expect(user.getEmail().toString()).toBe('[email protected]');
});
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);
});
});
});
Application Layer Tests — Use Cases
Focus: Orchestration. Does the Use Case fetch data, call the right Entity method, save it, and publish events?
Mocking Ports
// 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: '[email protected]', password: 'StrongPass1!' });
expect(userRepo.save).toHaveBeenCalledOnce();
});
it('publishes a user.registered event', async () => {
await useCase.execute({ email: '[email protected]', 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: '[email protected]', 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: '[email protected]', password: 'StrongPass1!' }),
).rejects.toThrow('DB error');
expect(publisher.publish).not.toHaveBeenCalled();
});
});
Execution Workflow (Red-Green-Refactor)
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
Useful NPM Scripts
// package.json
{
"scripts": {
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage"
}
}
Anti-Patterns
| ❌ 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 |
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.
Gives 2 of the 12 instructions most tdd skills give in ~1.9k tokens
Counted across 439 of the 443 authors here whose files we hold, read 2026-08-07
- Write minimal code to pass the testhere, and in 304 of 439, across 222 files
- Write a failing test firsthere, and in 174 of 439, across 111 files
- Refactor code only after tests passin 172 of 439, across 102 files
- Watch the test fail before writing codein 145 of 439, across 97 files
- Test one behavior per testin 108 of 439, across 46 files
- Refactor code while keeping tests greenin 100 of 439, across 88 files
- Delete code written before testsin 99 of 439, across 55 files
- Run tests after each refactor stepin 88 of 439, across 57 files
- Confirm the test fails for the right reasonin 66 of 439, across 62 files
- Use real code instead of mocks unless unavoidablein 60 of 439, across 17 files
- Reproduce bugs with a test before fixingin 53 of 439, across 36 files
- Write tests before implementationin 51 of 439, across 43 files
Said here and by no other author read
- mock repository and publisher interfaces
Grouped from the skills themselves: near-identical wordings counted once, and counted by distinct author, so one author publishing three of these counts once. Length counted with cl100k_base; the agent that loads this file may tokenize it differently.