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:
- Domain layer là core - Test kỹ nhất, coverage cao nhất
- Test behavior, không test implementation
- Fast feedback loop - Unit tests nhanh, reliable
- Real scenarios - Integration tests cover realistic workflows
- Test pyramid - Nhiều unit tests, ít E2E tests