Bỏ qua

DDD Testing Strategy - Chiến Lược Test Hiệu Quả

1. Test Pyramid Trong DDD

graph TD
    A[" UI/E2E Tests<br/>(Ít, chậm, đắt)"] 
    B[" Integration Tests<br/>(Vừa phải)"]
    C[" Unit Tests<br/>(Nhiều, nhanh, rẻ)"]

    A --> B
    B --> C

    D["Domain Logic Tests"] --> C
    E["Application Service Tests"] --> B
    F["Infrastructure Tests"] --> B
    G["End-to-End Workflow Tests"] --> A

Phân Bổ Tests Trong DDD:

  • 70% Unit Tests: Domain logic, Value Objects, Entities
  • 20% Integration Tests: Application Services, Repository implementations
  • 10% E2E Tests: Complete business workflows

2. Testing Domain Layer - Phần Quan Trọng Nhất

Test Entity Behavior

Ví dụ: Test Order Entity

describe('Order Entity', () => {
    test('Should create valid order with items', () => {
        // Arrange
        const customerId = CustomerId.of('CUST_001');
        const items = [
            OrderItem.create('PROD_A', 2, Money.fromVND(500000)),
            OrderItem.create('PROD_B', 1, Money.fromVND(300000))
        ];

        // Act
        const order = Order.create(customerId, items);

        // Assert
        expect(order.totalAmount).toEqual(Money.fromVND(1300000));
        expect(order.status).toBe(OrderStatus.PENDING);
        expect(order.itemCount).toBe(3);
    });

    test('Should reject order exceeding amount limit', () => {
        // Arrange
        const customerId = CustomerId.of('CUST_001');
        const expensiveItems = [
            OrderItem.create('PROD_X', 1, Money.fromVND(150000000))
        ];

        // Act & Assert
        expect(() => {
            Order.create(customerId, expensiveItems);
        }).toThrow('Order amount exceeds limit of 100,000,000 VND');
    });

    test('Should transition status correctly', () => {
        // Arrange
        const order = createValidOrder();

        // Act
        order.confirm();

        // Assert
        expect(order.status).toBe(OrderStatus.CONFIRMED);
        expect(order.confirmedAt).toBeDefined();
    });

    test('Should not allow invalid status transitions', () => {
        // Arrange
        const order = createValidOrder();
        order.confirm();
        order.ship();

        // Act & Assert
        expect(() => {
            order.confirm(); // Cannot confirm shipped order
        }).toThrow('Cannot transition from SHIPPED to CONFIRMED');
    });
});

Test Value Objects

Ví dụ: Test Money Value Object

describe('Money Value Object', () => {
    test('Should create valid money with currency', () => {
        // Act
        const money = Money.fromVND(100000);

        // Assert
        expect(money.amount).toBe(100000);
        expect(money.currency).toBe('VND');
    });

    test('Should add money correctly', () => {
        // Arrange
        const money1 = Money.fromVND(100000);
        const money2 = Money.fromVND(50000);

        // Act
        const result = money1.add(money2);

        // Assert
        expect(result.amount).toBe(150000);
        expect(result.currency).toBe('VND');
    });

    test('Should not add different currencies', () => {
        // Arrange
        const vnd = Money.fromVND(100000);
        const usd = Money.fromUSD(100);

        // Act & Assert
        expect(() => {
            vnd.add(usd);
        }).toThrow('Cannot add different currencies');
    });

    test('Should compare money correctly', () => {
        // Arrange
        const money1 = Money.fromVND(100000);
        const money2 = Money.fromVND(100000);
        const money3 = Money.fromVND(200000);

        // Assert
        expect(money1.equals(money2)).toBe(true);
        expect(money1.equals(money3)).toBe(false);
        expect(money1.isLessThan(money3)).toBe(true);
    });
});

Test Aggregate Invariants

Ví dụ: Test Order Aggregate Rules

describe('Order Aggregate Invariants', () => {
    test('Should maintain total amount consistency when adding items', () => {
        // Arrange
        const order = createOrderWithItems([
            OrderItem.create('PROD_A', 1, Money.fromVND(100000))
        ]);
        const newItem = OrderItem.create('PROD_B', 2, Money.fromVND(50000));

        // Act
        order.addItem(newItem);

        // Assert
        expect(order.totalAmount).toEqual(Money.fromVND(200000));
        expect(order.items).toHaveLength(2);
    });

    test('Should not allow adding item that exceeds order limit', () => {
        // Arrange
        const order = createOrderWithItems([
            OrderItem.create('PROD_A', 1, Money.fromVND(99000000))
        ]);
        const expensiveItem = OrderItem.create('PROD_B', 1, Money.fromVND(2000000));

        // Act & Assert
        expect(() => {
            order.addItem(expensiveItem);
        }).toThrow('Adding item would exceed order limit');
    });

    test('Should not allow duplicate products in order', () => {
        // Arrange
        const order = createOrderWithItems([
            OrderItem.create('PROD_A', 1, Money.fromVND(100000))
        ]);
        const duplicateItem = OrderItem.create('PROD_A', 2, Money.fromVND(100000));

        // Act & Assert
        expect(() => {
            order.addItem(duplicateItem);
        }).toThrow('Product PROD_A already exists in order');
    });
});

3. Testing Application Services

Test Use Cases With Mocked Dependencies

Ví dụ: Test OrderApplicationService

describe('OrderApplicationService', () => {
    let orderService: OrderApplicationService;
    let mockOrderRepo: jest.Mocked<OrderRepository>;
    let mockInventoryService: jest.Mocked<InventoryService>;
    let mockEventBus: jest.Mocked<EventBus>;

    beforeEach(() => {
        mockOrderRepo = createMockOrderRepository();
        mockInventoryService = createMockInventoryService();
        mockEventBus = createMockEventBus();

        orderService = new OrderApplicationService(
            mockOrderRepo,
            mockInventoryService,
            mockEventBus
        );
    });

    test('Should create order successfully when items are available', async () => {
        // Arrange
        const command = new CreateOrderCommand(
            'CUST_001',
            [
                { productId: 'PROD_A', quantity: 2 },
                { productId: 'PROD_B', quantity: 1 }
            ]
        );

        mockInventoryService.checkAvailability.mockResolvedValue([
            { productId: 'PROD_A', available: true, price: Money.fromVND(500000) },
            { productId: 'PROD_B', available: true, price: Money.fromVND(300000) }
        ]);

        // Act
        const result = await orderService.createOrder(command);

        // Assert
        expect(result.orderId).toBeDefined();
        expect(mockOrderRepo.save).toHaveBeenCalledWith(
            expect.objectContaining({
                customerId: CustomerId.of('CUST_001'),
                totalAmount: Money.fromVND(1300000)
            })
        );
        expect(mockEventBus.publish).toHaveBeenCalledWith(
            expect.any(OrderPlacedEvent)
        );
    });

    test('Should reject order when items are not available', async () => {
        // Arrange
        const command = new CreateOrderCommand('CUST_001', [
            { productId: 'PROD_X', quantity: 1 }
        ]);

        mockInventoryService.checkAvailability.mockResolvedValue([
            { productId: 'PROD_X', available: false }
        ]);

        // Act & Assert
        await expect(orderService.createOrder(command))
            .rejects.toThrow('Product PROD_X is not available');

        expect(mockOrderRepo.save).not.toHaveBeenCalled();
        expect(mockEventBus.publish).not.toHaveBeenCalled();
    });

    test('Should handle payment processing failure', async () => {
        // Arrange
        const command = new ProcessPaymentCommand('ORD_001', 'CARD_123');
        const order = createPendingOrder();

        mockOrderRepo.findById.mockResolvedValue(order);
        mockPaymentService.processPayment.mockRejectedValue(
            new PaymentFailedException('Insufficient funds')
        );

        // Act
        await orderService.processPayment(command);

        // Assert
        expect(order.status).toBe(OrderStatus.PAYMENT_FAILED);
        expect(mockEventBus.publish).toHaveBeenCalledWith(
            expect.any(PaymentFailedEvent)
        );
    });
});

4. Testing Infrastructure Layer

Test Repository Implementations

Ví dụ: Test SqlOrderRepository

describe('SqlOrderRepository', () => {
    let repository: SqlOrderRepository;
    let database: TestDatabase;

    beforeEach(async () => {
        database = await createTestDatabase();
        repository = new SqlOrderRepository(database);
    });

    afterEach(async () => {
        await database.cleanup();
    });

    test('Should save and retrieve order correctly', async () => {
        // Arrange
        const order = Order.create(
            CustomerId.of('CUST_001'),
            [OrderItem.create('PROD_A', 2, Money.fromVND(500000))]
        );

        // Act
        await repository.save(order);
        const retrieved = await repository.findById(order.id);

        // Assert
        expect(retrieved).not.toBeNull();
        expect(retrieved!.id).toEqual(order.id);
        expect(retrieved!.totalAmount).toEqual(order.totalAmount);
        expect(retrieved!.items).toHaveLength(1);
    });

    test('Should find orders by customer correctly', async () => {
        // Arrange
        const customerId = CustomerId.of('CUST_001');
        const order1 = createOrderForCustomer(customerId);
        const order2 = createOrderForCustomer(customerId);
        const order3 = createOrderForCustomer(CustomerId.of('CUST_002'));

        await repository.save(order1);
        await repository.save(order2);
        await repository.save(order3);

        // Act
        const customerOrders = await repository.findByCustomerId(customerId);

        // Assert
        expect(customerOrders).toHaveLength(2);
        expect(customerOrders.map(o => o.id)).toContain(order1.id);
        expect(customerOrders.map(o => o.id)).toContain(order2.id);
        expect(customerOrders.map(o => o.id)).not.toContain(order3.id);
    });

    test('Should handle concurrent updates correctly', async () => {
        // Arrange
        const order = createValidOrder();
        await repository.save(order);

        // Act - Simulate concurrent updates
        const order1 = await repository.findById(order.id);
        const order2 = await repository.findById(order.id);

        order1!.addItem(OrderItem.create('PROD_X', 1, Money.fromVND(100000)));
        order2!.addItem(OrderItem.create('PROD_Y', 1, Money.fromVND(200000)));

        await repository.save(order1!);

        // Assert - Second save should detect conflict
        await expect(repository.save(order2!))
            .rejects.toThrow('Optimistic locking conflict');
    });
});

Test Event Publishing

Ví dụ: Test Event Bus Integration

describe('Event Bus Integration', () => {
    let eventBus: EventBus;
    let mockHandlers: jest.Mocked<EventHandler>[];

    beforeEach(() => {
        mockHandlers = [
            createMockEventHandler('InventoryHandler'),
            createMockEventHandler('EmailHandler'),
            createMockEventHandler('AnalyticsHandler')
        ];

        eventBus = new InMemoryEventBus();
        mockHandlers.forEach(handler => eventBus.subscribe(handler));
    });

    test('Should publish event to all subscribed handlers', async () => {
        // Arrange
        const event = new OrderPlacedEvent(
            'ORD_001',
            'CUST_001',
            Money.fromVND(1000000),
            new Date()
        );

        // Act
        await eventBus.publish(event);

        // Assert
        mockHandlers.forEach(handler => {
            expect(handler.handle).toHaveBeenCalledWith(event);
        });
    });

    test('Should handle handler failures gracefully', async () => {
        // Arrange
        const event = new OrderPlacedEvent('ORD_001', 'CUST_001', Money.fromVND(1000000), new Date());
        mockHandlers[1].handle.mockRejectedValue(new Error('Email service down'));

        // Act
        await eventBus.publish(event);

        // Assert - Other handlers should still be called
        expect(mockHandlers[0].handle).toHaveBeenCalled();
        expect(mockHandlers[2].handle).toHaveBeenCalled();

        // Failed handler should be retried
        expect(mockHandlers[1].handle).toHaveBeenCalledTimes(3); // 1 + 2 retries
    });
});

5. Integration Testing Strategies

Test Complete Workflows

Ví dụ: End-to-End Order Processing

describe('Order Processing Workflow', () => {
    let app: TestApplication;

    beforeEach(async () => {
        app = await createTestApplication();
        await app.seedTestData({
            customers: [
                { id: 'CUST_001', name: 'John Doe', email: 'john@test.com' }
            ],
            products: [
                { id: 'PROD_A', name: 'iPhone 15', price: 25000000, stock: 10 },
                { id: 'PROD_B', name: 'AirPods', price: 5000000, stock: 5 }
            ]
        });
    });

    afterEach(async () => {
        await app.cleanup();
    });

    test('Should complete full order workflow successfully', async () => {
        // 1. Place Order
        const createOrderResponse = await app.post('/api/orders', {
            customerId: 'CUST_001',
            items: [
                { productId: 'PROD_A', quantity: 1 },
                { productId: 'PROD_B', quantity: 2 }
            ]
        });

        expect(createOrderResponse.status).toBe(201);
        const orderId = createOrderResponse.body.orderId;

        // 2. Verify Order Created
        const orderResponse = await app.get(`/api/orders/${orderId}`);
        expect(orderResponse.body).toMatchObject({
            id: orderId,
            customerId: 'CUST_001',
            status: 'PENDING',
            totalAmount: 35000000,
            items: expect.arrayContaining([
                expect.objectContaining({ productId: 'PROD_A', quantity: 1 }),
                expect.objectContaining({ productId: 'PROD_B', quantity: 2 })
            ])
        });

        // 3. Verify Inventory Reserved
        const inventoryResponse = await app.get('/api/inventory/PROD_A');
        expect(inventoryResponse.body.availableStock).toBe(9); // 10 - 1

        // 4. Process Payment
        const paymentResponse = await app.post(`/api/orders/${orderId}/payment`, {
            paymentMethodId: 'CARD_123',
            amount: 35000000
        });

        expect(paymentResponse.status).toBe(200);

        // 5. Verify Order Confirmed
        const confirmedOrderResponse = await app.get(`/api/orders/${orderId}`);
        expect(confirmedOrderResponse.body.status).toBe('CONFIRMED');

        // 6. Verify Email Sent (check outbox)
        const emailsResponse = await app.get('/api/test/emails');
        expect(emailsResponse.body).toContainEqual(
            expect.objectContaining({
                to: 'john@test.com',
                subject: expect.stringContaining('Order Confirmation'),
                content: expect.stringContaining(orderId)
            })
        );

        // 7. Ship Order
        const shipmentResponse = await app.post(`/api/orders/${orderId}/ship`, {
            trackingNumber: 'TRACK_123',
            carrier: 'ViettelPost'
        });

        expect(shipmentResponse.status).toBe(200);

        // 8. Verify Final State
        const finalOrderResponse = await app.get(`/api/orders/${orderId}`);
        expect(finalOrderResponse.body.status).toBe('SHIPPED');
        expect(finalOrderResponse.body.trackingNumber).toBe('TRACK_123');
    });

    test('Should handle out of stock scenario correctly', async () => {
        // Arrange - Order more than available stock
        const createOrderRequest = {
            customerId: 'CUST_001',
            items: [{ productId: 'PROD_B', quantity: 10 }] // Only 5 in stock
        };

        // Act
        const response = await app.post('/api/orders', createOrderRequest);

        // Assert
        expect(response.status).toBe(400);
        expect(response.body.error).toBe('Insufficient stock for product PROD_B');

        // Verify no inventory was reserved
        const inventoryResponse = await app.get('/api/inventory/PROD_B');
        expect(inventoryResponse.body.availableStock).toBe(5); // Unchanged
    });

    test('Should handle payment failure gracefully', async () => {
        // 1. Create Order
        const createOrderResponse = await app.post('/api/orders', {
            customerId: 'CUST_001',
            items: [{ productId: 'PROD_A', quantity: 1 }]
        });

        const orderId = createOrderResponse.body.orderId;

        // 2. Simulate Payment Failure
        const paymentResponse = await app.post(`/api/orders/${orderId}/payment`, {
            paymentMethodId: 'INVALID_CARD',
            amount: 25000000
        });

        expect(paymentResponse.status).toBe(400);
        expect(paymentResponse.body.error).toBe('Payment failed: Invalid card');

        // 3. Verify Order Status
        const orderResponse = await app.get(`/api/orders/${orderId}`);
        expect(orderResponse.body.status).toBe('PAYMENT_FAILED');

        // 4. Verify Inventory Released
        const inventoryResponse = await app.get('/api/inventory/PROD_A');
        expect(inventoryResponse.body.availableStock).toBe(10); // Stock restored
    });
});

6. Test Data Builders - Giúp Test Dễ Đọc

Builder Pattern Cho Test Data

// Order Builder
class OrderTestBuilder {
    private customerId = CustomerId.of('DEFAULT_CUSTOMER');
    private items: OrderItem[] = [];
    private status = OrderStatus.PENDING;

    withCustomer(customerId: string): OrderTestBuilder {
        this.customerId = CustomerId.of(customerId);
        return this;
    }

    withItem(productId: string, quantity: number, price: number): OrderTestBuilder {
        this.items.push(OrderItem.create(productId, quantity, Money.fromVND(price)));
        return this;
    }

    withStatus(status: OrderStatus): OrderTestBuilder {
        this.status = status;
        return this;
    }

    build(): Order {
        const order = Order.create(this.customerId, this.items);
        if (this.status !== OrderStatus.PENDING) {
            // Apply status changes
            this.applyStatusChange(order, this.status);
        }
        return order;
    }

    private applyStatusChange(order: Order, targetStatus: OrderStatus): void {
        switch (targetStatus) {
            case OrderStatus.CONFIRMED:
                order.confirm();
                break;
            case OrderStatus.SHIPPED:
                order.confirm();
                order.ship();
                break;
            // ... other status transitions
        }
    }
}

// Usage trong tests
test('Should calculate shipping cost for heavy orders', () => {
    // Arrange
    const heavyOrder = new OrderTestBuilder()
        .withCustomer('CUST_001')
        .withItem('PROD_HEAVY_1', 2, 1000000)
        .withItem('PROD_HEAVY_2', 1, 500000)
        .build();

    const address = new AddressTestBuilder()
        .inDistrict('District 1')
        .inCity('Ho Chi Minh City')
        .build();

    // Act
    const shippingCost = ShippingCalculator.calculate(heavyOrder, address);

    // Assert
    expect(shippingCost).toEqual(Money.fromVND(100000)); // Heavy item surcharge
});

Mother Objects Pattern

// Object Mothers - Pre-defined test objects
class OrderMother {
    static smallOrder(): Order {
        return new OrderTestBuilder()
            .withItem('PROD_SMALL', 1, 100000)
            .build();
    }

    static largeOrder(): Order {
        return new OrderTestBuilder()
            .withItem('PROD_EXPENSIVE', 1, 50000000)
            .withItem('PROD_PREMIUM', 2, 25000000)
            .build();
    }

    static confirmedOrder(): Order {
        return new OrderTestBuilder()
            .withItem('PROD_BASIC', 2, 500000)
            .withStatus(OrderStatus.CONFIRMED)
            .build();
    }

    static orderExceedingLimit(): Order {
        return new OrderTestBuilder()
            .withItem('PROD_ULTRA_EXPENSIVE', 1, 150000000)
            .build();
    }
}

// Usage
test('Should apply discount for large orders', () => {
    const order = OrderMother.largeOrder();
    const discount = DiscountCalculator.calculate(order);
    expect(discount.percentage).toBe(0.15); // 15% for large orders
});

7. Test Doubles - Mock Strategy

Khi Nào Dùng Loại Mock Nào

Stub - Cho queries, không verify calls

const stubCustomerRepo = {
    findById: jest.fn().mockResolvedValue(CustomerMother.premiumCustomer())
};

Mock - Cho commands, verify behavior

const mockEventBus = {
    publish: jest.fn()
};

// Verify
expect(mockEventBus.publish).toHaveBeenCalledWith(
    expect.any(OrderPlacedEvent)
);

Spy - Cho real objects, monitor calls

const realOrderRepo = new InMemoryOrderRepository();
const spyOrderRepo = jest.spyOn(realOrderRepo, 'save');

// Use real implementation but verify calls
expect(spyOrderRepo).toHaveBeenCalledTimes(1);

Fake - Cho complex dependencies

class FakeEmailService implements EmailService {
    private sentEmails: Email[] = [];

    async send(email: Email): Promise<void> {
        this.sentEmails.push(email);
    }

    getSentEmails(): Email[] {
        return [...this.sentEmails];
    }
}


8. Performance Testing

Load Testing Domain Logic

describe('Order Performance Tests', () => {
    test('Should handle large number of order items efficiently', () => {
        // Arrange
        const manyItems = Array.from({ length: 1000 }, (_, i) =>
            OrderItem.create(`PROD_${i}`, 1, Money.fromVND(100000))
        );

        // Act
        const startTime = performance.now();
        const order = Order.create(CustomerId.of('CUST_001'), manyItems);
        const endTime = performance.now();

        // Assert
        expect(order.itemCount).toBe(1000);
        expect(endTime - startTime).toBeLessThan(100); // Should complete in <100ms
    });

    test('Should calculate complex discounts efficiently', () => {
        const order = OrderMother.largeOrderWithManyItems(500);
        const customer = CustomerMother.premiumCustomerWithLongHistory();

        const startTime = performance.now();
        const discount = DiscountCalculator.calculate(order, customer);
        const endTime = performance.now();

        expect(discount).toBeDefined();
        expect(endTime - startTime).toBeLessThan(50);
    });
});

9. Testing Anti-Patterns - Tránh Những Lỗi Này

Testing Implementation Details

// SAI - Test implementation
test('Order should have items array property', () => {
    const order = new Order();
    expect(order.items).toBeInstanceOf(Array);  // Testing implementation
});

// ĐÚNG - Test behavior
test('Should allow adding items to order', () => {
    const order = OrderMother.emptyOrder();
    const item = OrderItemMother.validItem();

    order.addItem(item);

    expect(order.itemCount).toBe(1);  // Testing behavior
});

Fragile Tests

// SAI - Test depends on exact data
test('Should return customer orders', async () => {
    const orders = await orderService.getCustomerOrders('CUST_001');
    expect(orders).toHaveLength(3);  // Fragile - depends on test data
    expect(orders[0].id).toBe('ORD_001');  // Fragile - depends on order
});

// ĐÚNG - Test behavior
test('Should return only orders for specified customer', async () => {
    // Arrange - Create known test data
    const customerId = 'CUST_001';
    const customerOrder = await createOrderForCustomer(customerId);
    const otherOrder = await createOrderForCustomer('CUST_002');

    // Act
    const orders = await orderService.getCustomerOrders(customerId);

    // Assert
    expect(orders.map(o => o.customerId)).toEqual([customerId]);
    expect(orders.map(o => o.id)).toContain(customerOrder.id);
    expect(orders.map(o => o.id)).not.toContain(otherOrder.id);
});

Slow Tests

// SAI - Using real database for unit tests
test('Should create order', async () => {
    const realDatabase = new PostgresDatabase();  // Slow!
    const repo = new SqlOrderRepository(realDatabase);

    const order = OrderMother.validOrder();
    await repo.save(order);  // Slow database operation

    const saved = await repo.findById(order.id);
    expect(saved).toBeDefined();
});

// ĐÚNG - Use in-memory for unit tests
test('Should create order', () => {
    const repo = new InMemoryOrderRepository();  // Fast!

    const order = OrderMother.validOrder();
    repo.save(order);

    const saved = repo.findById(order.id);
    expect(saved).toBeDefined();
});

10. Test Organization & Best Practices

File Structure

tests/
├── unit/
│   ├── domain/
│   │   ├── order/
│   │   │   ├── Order.test.ts
│   │   │   ├── OrderItem.test.ts
│   │   │   └── OrderDomainService.test.ts
│   │   └── customer/
│   │       └── Customer.test.ts
│   └── application/
│       └── OrderApplicationService.test.ts
├── integration/
│   ├── repositories/
│   │   └── SqlOrderRepository.test.ts
│   └── workflows/
│       └── OrderProcessingWorkflow.test.ts
├── e2e/
│   └── OrderE2E.test.ts
└── helpers/
    ├── builders/
    │   ├── OrderTestBuilder.ts
    │   └── CustomerTestBuilder.ts
    ├── mothers/
    │   ├── OrderMother.ts
    │   └── CustomerMother.ts
    └── TestApplication.ts

Test Naming Convention

// Pattern: Should [expected behavior] When [condition]
describe('OrderDomainService', () => {
    describe('calculateDiscount', () => {
        test('Should apply 10% discount When customer has more than 5 orders', () => {
            // ...
        });

        test('Should apply no discount When customer is new', () => {
            // ...
        });

        test('Should throw error When customer is not found', () => {
            // ...
        });
    });
});

Test Documentation

/**
 * Test suite for Order Aggregate
 * 
 * Covers business rules:
 * - BR001: Order total cannot exceed 100M VND
 * - BR002: Order must have at least one item
 * - BR003: Order status transitions must follow workflow
 * - BR004: Items in order must be available in inventory
 */
describe('Order Aggregate Business Rules', () => {
    // Tests for BR001
    describe('Order total limit', () => {
        test('Should reject order When total exceeds 100M VND', () => {
            // This test covers BR001
        });
    });
});

11. Continuous Testing Strategy

Test Automation Pipeline

# CI/CD Pipeline for DDD Tests
name: DDD Test Pipeline

on: [push, pull_request]

jobs:
  unit-tests:
    runs-on: ubuntu-latest
    steps:
      - name: Run Domain Unit Tests
        run: npm test -- --testPathPattern=unit/domain
      - name: Run Application Unit Tests  
        run: npm test -- --testPathPattern=unit/application

  integration-tests:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:13
    steps:
      - name: Run Repository Tests
        run: npm test -- --testPathPattern=integration/repositories
      - name: Run Workflow Tests
        run: npm test -- --testPathPattern=integration/workflows

  e2e-tests:
    runs-on: ubuntu-latest
    steps:
      - name: Start Test Environment
        run: docker-compose up -d
      - name: Run E2E Tests
        run: npm run test:e2e
      - name: Cleanup
        run: docker-compose down

Test Coverage Goals

  • Domain Layer: 95%+ coverage (business logic critical)
  • Application Layer: 85%+ coverage (orchestration logic)
  • Infrastructure Layer: 70%+ coverage (focus on complex logic)

12. Kết Luận

Testing trong DDD:

  1. Domain layer là core - Test kỹ nhất, coverage cao nhất
  2. Test behavior, không test implementation
  3. Fast feedback loop - Unit tests nhanh, reliable
  4. Real scenarios - Integration tests cover realistic workflows
  5. Test pyramid - Nhiều unit tests, ít E2E tests