Back to catalog

DDD Testing Standards

Standards de tests exhaustifs pour les bounded contexts DDD (Domain, Application, Integration). À utiliser lors de l'écriture de tests backend, tests unitaires, tests d'intégration, ou quand l'utilisateur mentionne "test", "TDD", "coverage", "unit test", "integration test", "test domain", "test handler".

  • Category: discovered
  • Tags: github, community
  • Version: 1.0.0
  • Author: walid
  • Downloads: 0
  • Updated: 2025-11-22

Download the latest version (v1.0.0) from this page.

SKILL.md

---
name: DDD Testing Standards
description: Standards de tests exhaustifs pour les bounded contexts DDD (Domain, Application, Integration). À utiliser lors de l'écriture de tests backend, tests unitaires, tests d'intégration, ou quand l'utilisateur mentionne "test", "TDD", "coverage", "unit test", "integration test", "test domain", "test handler".
allowed-tools: [Read, Write, Edit, Glob, Grep, Bash]
---

# DDD Testing Standards

## 🎯 Mission

Créer des tests exhaustifs pour les bounded contexts DDD suivant une approche TDD (Test-Driven Development) avec des standards de coverage stricts.

## 🏆 Philosophie Critique

**Dans DDD, les tests sont NON-NÉGOCIABLES.**

La logique métier dans le Domain Layer DOIT être testée à 100% avant toute autre implémentation. Les tests sont la **documentation vivante** de votre logique métier.

### Pourquoi TDD en DDD ?

1. **Logique métier fiable** : Le Domain contient les règles critiques de l'application
2. **Refactoring sécurisé** : Tests exhaustifs permettent de refactorer sans casser
3. **Documentation** : Tests décrivent le comportement attendu
4. **Confidence** : Déploiement en production sans peur
5. **Régression** : Prévenir la réintroduction de bugs

## 📁 Structure des Tests

```
bounded-context/
├── tests/
│   ├── unit/
│   │   ├── domain/
│   │   │   ├── entities/
│   │   │   │   ├── subscription.entity.spec.ts
│   │   │   │   └── club.entity.spec.ts
│   │   │   ├── value-objects/
│   │   │   │   ├── subscription-plan.vo.spec.ts
│   │   │   │   └── club-name.vo.spec.ts
│   │   │   └── services/
│   │   │       └── subscription-limit.service.spec.ts
│   │   └── application/
│   │       ├── commands/
│   │       │   ├── create-club.handler.spec.ts
│   │       │   └── subscribe-to-plan.handler.spec.ts
│   │       └── queries/
│   │           ├── get-club.handler.spec.ts
│   │           └── list-clubs.handler.spec.ts
│   └── integration/
│       └── handlers/
│           ├── create-club.integration.spec.ts
│           └── subscribe-to-plan.integration.spec.ts
```

## 🧪 1. Domain Layer Tests (MANDATORY - 100% Coverage)

### Entities Tests

**Objectif** : Tester TOUTE la logique métier encapsulée dans les entités

**Ce qui DOIT être testé** :
- ✅ Tous les business methods
- ✅ Toutes les validation rules
- ✅ Toutes les state transitions
- ✅ Tous les edge cases et boundary conditions
- ✅ Tous les invariants
- ✅ Tous les factory methods

#### Template Entity Test

```typescript
// tests/unit/domain/entities/subscription.entity.spec.ts

import { Subscription } from '../../../../domain/entities/subscription.entity';
import { SubscriptionPlan } from '../../../../domain/value-objects/subscription-plan.vo';
import { SubscriptionStatus } from '../../../../domain/value-objects/subscription-status.vo';

describe('Subscription Entity', () => {
  describe('Factory Method - create()', () => {
    it('should create a new subscription with default values', () => {
      // Arrange
      const clubId = 'club-123';
      const plan = SubscriptionPlan.FREE;

      // Act
      const subscription = Subscription.create(clubId, plan);

      // Assert
      expect(subscription.getClubId()).toBe(clubId);
      expect(subscription.getPlan()).toBe(plan);
      expect(subscription.isActive()).toBe(true);
      expect(subscription.getCurrentTeamsCount()).toBe(0);
    });

    it('should throw error when clubId is empty', () => {
      // Arrange
      const clubId = '';
      const plan = SubscriptionPlan.FREE;

      // Act & Assert
      expect(() => Subscription.create(clubId, plan)).toThrow('Club ID is required');
    });
  });

  describe('Business Method - canCreateTeam()', () => {
    it('should return true when subscription is active and limit not reached', () => {
      // Arrange
      const subscription = Subscription.create('club-123', SubscriptionPlan.FREE);

      // Act
      const result = subscription.canCreateTeam();

      // Assert
      expect(result).toBe(true);
    });

    it('should return false when subscription is inactive', () => {
      // Arrange
      const subscription = Subscription.create('club-123', SubscriptionPlan.FREE);
      subscription.deactivate(); // State transition

      // Act
      const result = subscription.canCreateTeam();

      // Assert
      expect(result).toBe(false);
    });

    it('should return false when team limit is reached', () => {
      // Arrange
      const subscription = Subscription.create('club-123', SubscriptionPlan.FREE);
      subscription.incrementTeamsCount(); // 1/1 team for FREE plan

      // Act
      const result = subscription.canCreateTeam();

      // Assert
      expect(result).toBe(false);
    });

    it('should return true when plan has unlimited teams', () => {
      // Arrange
      const subscription = Subscription.create('club-123', SubscriptionPlan.UNLIMITED);
      // Simulate many teams
      for (let i = 0; i < 100; i++) {
        subscription.incrementTeamsCount();
      }

      // Act
      const result = subscription.canCreateTeam();

      // Assert
      expect(result).toBe(true);
    });

    it('should handle null teams count gracefully', () => {
      // Arrange
      const subscription = Subscription.create('club-123', SubscriptionPlan.FREE);

      // Act
      const result = subscription.canCreateTeam();

      // Assert
      expect(result).toBe(true);
    });
  });

  describe('Business Method - upgrade()', () => {
    it('should upgrade from FREE to PRO successfully', () => {
      // Arrange
      const subscription = Subscription.create('club-123', SubscriptionPlan.FREE);

      // Act
      subscription.upgrade(SubscriptionPlan.PRO);

      // Assert
      expect(subscription.getPlan()).toBe(SubscriptionPlan.PRO);
    });

    it('should upgrade from PRO to UNLIMITED successfully', () => {
      // Arrange
      const subscription = Subscription.create('club-123', SubscriptionPlan.PRO);

      // Act
      subscription.upgrade(SubscriptionPlan.UNLIMITED);

      // Assert
      expect(subscription.getPlan()).toBe(SubscriptionPlan.UNLIMITED);
    });

    it('should throw error when trying to downgrade', () => {
      // Arrange
      const subscription = Subscription.create('club-123', SubscriptionPlan.PRO);

      // Act & Assert
      expect(() => subscription.upgrade(SubscriptionPlan.FREE))
        .toThrow('Cannot downgrade subscription');
    });

    it('should throw error when upgrading to same plan', () => {
      // Arrange
      const subscription = Subscription.create('club-123', SubscriptionPlan.PRO);

      // Act & Assert
      expect(() => subscription.upgrade(SubscriptionPlan.PRO))
        .toThrow('Already on this plan');
    });
  });

  describe('State Transition - incrementTeamsCount()', () => {
    it('should increment teams count when limit not reached', () => {
      // Arrange
      const subscription = Subscription.create('club-123', SubscriptionPlan.PRO);
      const initialCount = subscription.getCurrentTeamsCount();

      // Act
      subscription.incrementTeamsCount();

      // Assert
      expect(subscription.getCurrentTeamsCount()).toBe(initialCount + 1);
    });

    it('should throw error when limit is reached', () => {
      // Arrange
      const subscription = Subscription.create('club-123', SubscriptionPlan.FREE);
      subscription.incrementTeamsCount(); // Reach limit (1/1)

      // Act & Assert
      expect(() => subscription.incrementTeamsCount())
        .toThrow('Team limit reached for current plan');
    });

    it('should allow unlimited increments for UNLIMITED plan', () => {
      // Arrange
      const subscription = Subscription.create('club-123', SubscriptionPlan.UNLIMITED);

      // Act - Increment 1000 times
      for (let i = 0; i < 1000; i++) {
        subscription.incrementTeamsCount();
      }

      // Assert
      expect(subscription.getCurrentTeamsCount()).toBe(1000);
    });
  });

  describe('Edge Cases', () => {
    it('should handle negative teams count validation', () => {
      // Arrange & Act & Assert
      expect(() => new Subscription(
        'id-123',
        'club-123',
        SubscriptionPlan.FREE,
        SubscriptionStatus.ACTIVE,
        new Date(),
        null,
        -1, // Negative count
      )).toThrow('Teams count cannot be negative');
    });

    it('should handle null plan', () => {
      // Arrange & Act & Assert
      expect(() => new Subscription(
        'id-123',
        'club-123',
        null as any,
        SubscriptionStatus.ACTIVE,
        new Date(),
        null,
        0,
      )).toThrow('Plan is required');
    });
  });
});
```

### Value Objects Tests

```typescript
// tests/unit/domain/value-objects/subscription-plan.vo.spec.ts

import { SubscriptionPlan } from '../../../../domain/value-objects/subscription-plan.vo';

describe('SubscriptionPlan Value Object', () => {
  describe('Creation', () => {
    it('should create FREE plan', () => {
      // Act
      const plan = SubscriptionPlan.FREE;

      // Assert
      expect(plan.toString()).toBe('FREE');
      expect(plan.getMaxTeams()).toBe(1);
    });

    it('should throw error for invalid plan name', () => {
      // Act & Assert
      expect(() => SubscriptionPlan.fromString('INVALID'))
        .toThrow('Invalid plan: INVALID');
    });
  });

  describe('hasTeamLimit()', () => {
    it('should return true for FREE plan', () => {
      expect(SubscriptionPlan.FREE.hasTeamLimit()).toBe(true);
    });

    it('should return false for UNLIMITED plan', () => {
      expect(SubscriptionPlan.UNLIMITED.hasTeamLimit()).toBe(false);
    });
  });

  describe('isUpgradeFrom()', () => {
    it('should return true when upgrading from FREE to PRO', () => {
      expect(SubscriptionPlan.PRO.isUpgradeFrom(SubscriptionPlan.FREE)).toBe(true);
    });

    it('should return false when downgrading from PRO to FREE', () => {
      expect(SubscriptionPlan.FREE.isUpgradeFrom(SubscriptionPlan.PRO)).toBe(false);
    });

    it('should return false for same plan', () => {
      expect(SubscriptionPlan.PRO.isUpgradeFrom(SubscriptionPlan.PRO)).toBe(false);
    });
  });

  describe('Immutability', () => {
    it('should be immutable (same instance for same value)', () => {
      const plan1 = SubscriptionPlan.FREE;
      const plan2 = SubscriptionPlan.FREE;

      expect(plan1).toBe(plan2);
    });
  });
});
```

### Domain Services Tests

```typescript
// tests/unit/domain/services/subscription-limit.service.spec.ts

import { SubscriptionLimitService } from '../../../../domain/services/subscription-limit.service';
import { Subscription } from '../../../../domain/entities/subscription.entity';
import { SubscriptionPlan } from '../../../../domain/value-objects/subscription-plan.vo';

describe('SubscriptionLimitService', () => {
  let service: SubscriptionLimitService;

  beforeEach(() => {
    service = new SubscriptionLimitService();
  });

  describe('canAddTeam()', () => {
    it('should return true when subscription allows new team', () => {
      // Arrange
      const subscription = Subscription.create('club-123', SubscriptionPlan.FREE);

      // Act
      const result = service.canAddTeam(subscription);

      // Assert
      expect(result).toBe(true);
    });

    it('should return false when team limit is reached', () => {
      // Arrange
      const subscription = Subscription.create('club-123', SubscriptionPlan.FREE);
      subscription.incrementTeamsCount(); // Reach limit

      // Act
      const result = service.canAddTeam(subscription);

      // Assert
      expect(result).toBe(false);
    });
  });

  describe('getRemainingSlots()', () => {
    it('should return correct remaining slots for PRO plan', () => {
      // Arrange
      const subscription = Subscription.create('club-123', SubscriptionPlan.PRO);
      subscription.incrementTeamsCount(); // 1/3 teams

      // Act
      const remaining = service.getRemainingSlots(subscription);

      // Assert
      expect(remaining).toBe(2);
    });

    it('should return Infinity for UNLIMITED plan', () => {
      // Arrange
      const subscription = Subscription.create('club-123', SubscriptionPlan.UNLIMITED);

      // Act
      const remaining = service.getRemainingSlots(subscription);

      // Assert
      expect(remaining).toBe(Infinity);
    });
  });
});
```

## 🔧 2. Application Layer Tests (MANDATORY - 95%+ Coverage)

### Command Handler Tests

**Objectif** : Tester l'orchestration des entités domain par les handlers

**Ce qui DOIT être testé** :
- ✅ Successful execution path
- ✅ All validation errors
- ✅ Domain exceptions handling
- ✅ Repository methods are called correctly
- ✅ Return values (IDs)

#### Template Command Handler Test

```typescript
// tests/unit/application/commands/create-club.handler.spec.ts

import { CreateClubHandler } from '../../../../application/commands/create-club/create-club.handler';
import { CreateClubCommand } from '../../../../application/commands/create-club/create-club.command';
import { IClubRepository } from '../../../../domain/repositories/club.repository.interface';
import { Club } from '../../../../domain/entities/club.entity';

describe('CreateClubHandler', () => {
  let handler: CreateClubHandler;
  let mockClubRepository: jest.Mocked<IClubRepository>;

  beforeEach(() => {
    // Mock repository
    mockClubRepository = {
      create: jest.fn(),
      findById: jest.fn(),
      findByUserId: jest.fn(),
      update: jest.fn(),
      delete: jest.fn(),
    } as jest.Mocked<IClubRepository>;

    handler = new CreateClubHandler(mockClubRepository);
  });

  describe('execute()', () => {
    it('should create club successfully with valid data', async () => {
      // Arrange
      const command = new CreateClubCommand(
        'Volley Club Paris',
        'Best club in Paris',
        'user-123',
      );

      const mockClub = Club.create(
        command.name,
        command.description,
        command.userId,
      );

      mockClubRepository.create.mockResolvedValue(mockClub);

      // Act
      const result = await handler.execute(command);

      // Assert
      expect(result).toBe(mockClub.getId());
      expect(mockClubRepository.create).toHaveBeenCalledTimes(1);
      expect(mockClubRepository.create).toHaveBeenCalledWith(
        expect.objectContaining({
          getName: expect.any(Function),
          getDescription: expect.any(Function),
        }),
      );
    });

    it('should throw error when name is empty', async () => {
      // Arrange
      const command = new CreateClubCommand(
        '', // Empty name
        'Description',
        'user-123',
      );

      // Act & Assert
      await expect(handler.execute(command))
        .rejects
        .toThrow('Club name cannot be empty');
    });

    it('should throw error when userId is missing', async () => {
      // Arrange
      const command = new CreateClubCommand(
        'Volley Club',
        'Description',
        '', // Empty userId
      );

      // Act & Assert
      await expect(handler.execute(command))
        .rejects
        .toThrow('User ID is required');
    });

    it('should call repository.create() with correct club entity', async () => {
      // Arrange
      const command = new CreateClubCommand(
        'Volley Club Paris',
        'Best club',
        'user-123',
      );

      const mockClub = Club.create(command.name, command.description, command.userId);
      mockClubRepository.create.mockResolvedValue(mockClub);

      // Act
      await handler.execute(command);

      // Assert
      expect(mockClubRepository.create).toHaveBeenCalledWith(
        expect.objectContaining({
          getName: expect.any(Function),
        }),
      );

      const callArg = mockClubRepository.create.mock.calls[0][0];
      expect(callArg.getName().getValue()).toBe('Volley Club Paris');
    });

    it('should propagate repository errors', async () => {
      // Arrange
      const command = new CreateClubCommand('Club', 'Desc', 'user-123');
      mockClubRepository.create.mockRejectedValue(new Error('Database error'));

      // Act & Assert
      await expect(handler.execute(command))
        .rejects
        .toThrow('Database error');
    });
  });
});
```

### Query Handler Tests

```typescript
// tests/unit/application/queries/get-club.handler.spec.ts

import { GetClubHandler } from '../../../../application/queries/get-club/get-club.handler';
import { GetClubQuery } from '../../../../application/queries/get-club/get-club.query';
import { IClubRepository } from '../../../../domain/repositories/club.repository.interface';
import { Club } from '../../../../domain/entities/club.entity';
import { ClubDetailReadModel } from '../../../../application/read-models/club-detail.read-model';

describe('GetClubHandler', () => {
  let handler: GetClubHandler;
  let mockClubRepository: jest.Mocked<IClubRepository>;

  beforeEach(() => {
    mockClubRepository = {
      create: jest.fn(),
      findById: jest.fn(),
      update: jest.fn(),
      delete: jest.fn(),
    } as jest.Mocked<IClubRepository>;

    handler = new GetClubHandler(mockClubRepository);
  });

  describe('execute()', () => {
    it('should return club read model when club exists', async () => {
      // Arrange
      const query = new GetClubQuery('club-123');

      const mockClub = Club.create('Club Paris', 'Description', 'user-123');
      mockClubRepository.findById.mockResolvedValue(mockClub);

      // Act
      const result = await handler.execute(query);

      // Assert
      expect(result).toBeDefined();
      expect(result.id).toBe(mockClub.getId());
      expect(result.name).toBe('Club Paris');
      expect(mockClubRepository.findById).toHaveBeenCalledWith('club-123');
    });

    it('should throw NotFoundException when club does not exist', async () => {
      // Arrange
      const query = new GetClubQuery('non-existent-id');
      mockClubRepository.findById.mockResolvedValue(null);

      // Act & Assert
      await expect(handler.execute(query))
        .rejects
        .toThrow('Club with ID non-existent-id not found');
    });

    it('should transform domain entity to read model correctly', async () => {
      // Arrange
      const query = new GetClubQuery('club-123');
      const mockClub = Club.create('Club Paris', 'Best club', 'user-123');
      mockClubRepository.findById.mockResolvedValue(mockClub);

      // Act
      const result = await handler.execute(query);

      // Assert
      expect(result).toMatchObject({
        id: mockClub.getId(),
        name: 'Club Paris',
        description: 'Best club',
      });
    });
  });
});
```

## 🔗 3. Integration Tests (MANDATORY - Critical Workflows)

### Handler → Repository → Database Integration

**Objectif** : Tester le flux complet de bout en bout avec une vraie base de données

**Ce qui DOIT être testé** :
- ✅ Complete workflows: Handler → Repository → Database
- ✅ Transactions and rollbacks
- ✅ Concurrent operations
- ✅ Real database constraints

#### Template Integration Test

```typescript
// tests/integration/handlers/create-club.integration.spec.ts

import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import { PrismaService } from '../../../src/prisma/prisma.service';
import { CreateClubHandler } from '../../../src/club-management/application/commands/create-club/create-club.handler';
import { CreateClubCommand } from '../../../src/club-management/application/commands/create-club/create-club.command';
import { ClubRepository } from '../../../src/club-management/infrastructure/persistence/repositories/club.repository';
import { CLUB_REPOSITORY } from '../../../src/club-management/domain/repositories/club.repository.interface';

describe('CreateClubHandler Integration', () => {
  let app: INestApplication;
  let prismaService: PrismaService;
  let handler: CreateClubHandler;

  beforeAll(async () => {
    const moduleFixture: TestingModule = awai