| 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. |
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, or socket.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
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
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(, {
a = .();
b = .();
(a.(b)).();
});
});
Entity / Aggregate Test
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');
});
(, {
user = .(id, email, );
events = user.();
(events).();
(events[]).();
});
(, {
user = .(id, email, );
user.();
(user.()).();
});
});
(, {
(, {
user = .(id, email, );
user.();
});
(, {
user = .(id, email, );
( user.()).();
});
});
});
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
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),
save: vi.fn().mockResolvedValue(undefined),
});
const makePublisher = (): IRealTimePublisher => ({
publish: vi.fn(),
});
describe('RegisterUserUseCase', () => {
let userRepo: IUserRepository;
let publisher: IRealTimePublisher;
: ;
( {
userRepo = ();
publisher = ();
useCase = (userRepo, publisher);
});
(, () => {
useCase.({ : , : });
(userRepo.).();
});
(, () => {
useCase.({ : , : });
(publisher.).(, expect.({
: expect.({}),
}));
});
(, () => {
vi.(userRepo.).({ : vi.() } );
(
useCase.({ : , : }),
)..();
(userRepo.)..();
});
(, () => {
vi.(userRepo.).( ());
(
useCase.({ : , : }),
)..();
(publisher.)..();
});
});
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
{
"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 |