← 返回首页
🎯

DDD测试策略

📂 architecture ⏱ 2 min 308 words

DDD测试策略

领域层单元测试

领域层测试聚焦于业务规则的正确性,不依赖外部基础设施。

// 聚合根测试
@ExtendWith(MockitoExtension.class)
class OrderTest {
    
    @Test
    void shouldCreateOrderSuccessfully() {
        // Given
        Long customerId = 1L;
        List<OrderItem> items = List.of(
            OrderItem.create("P001", 2, Money.of(99.99)),
            OrderItem.create("P002", 1, Money.of(199.99))
        );
        
        // When
        Order order = Order.create(customerId, items);
        
        // Then
        assertThat(order.getStatus()).isEqualTo(OrderStatus.CREATED);
        assertThat(order.getItems()).hasSize(2);
        assertThat(order.calculateTotal()).isEqualTo(Money.of(399.97));
    }
    
    @Test
    void shouldRejectEmptyOrder() {
        // Given
        List<OrderItem> emptyItems = Collections.emptyList();
        
        // When & Then
        assertThatThrownBy(() -> Order.create(1L, emptyItems))
            .isInstanceOf(EmptyOrderException.class)
            .hasMessage("订单不能为空");
    }
    
    @Test
    void shouldTransitionToPaidStatus() {
        // Given
        Order order = Order.create(1L, List.of(
            OrderItem.create("P001", 1, Money.of(100))
        ));
        
        // When
        order.pay(Money.of(100));
        
        // Then
        assertThat(order.getStatus()).isEqualTo(OrderStatus.PAID);
        assertThat(order.getDomainEvents()).hasSize(2);
        assertThat(order.getDomainEvents().get(1))
            .isInstanceOf(OrderPaidEvent.class);
    }
    
    @Test
    void shouldRejectPaymentForShippedOrder() {
        // Given
        Order order = Order.create(1L, List.of(
            OrderItem.create("P001", 1, Money.of(100))
        ));
        order.pay(Money.of(100));
        order.ship();
        
        // When & Then
        assertThatThrownBy(() -> order.pay(Money.of(100)))
            .isInstanceOf(InvalidOrderStateException.class);
    }
}

// 值对象测试
class MoneyTest {
    
    @Test
    void shouldAddMoneyWithSameCurrency() {
        Money result = Money.of(100).add(Money.of(50));
        assertThat(result).isEqualTo(Money.of(150));
    }
    
    @Test
    void shouldRejectAdditionWithDifferentCurrency() {
        assertThatThrownBy(() -> 
            Money.of(100, Currency.USD).add(Money.of(50, Currency.EUR)))
            .isInstanceOf(CurrencyMismatchException.class);
    }
}

应用层集成测试

应用层测试验证服务编排和事务管理。

@SpringBootTest
@Transactional
class OrderApplicationServiceTest {
    
    @Autowired
    private OrderApplicationService orderService;
    
    @MockBean
    private EventPublisher eventPublisher;
    
    @Test
    void shouldCreateOrderAndPublishEvent() {
        // Given
        CreateOrderCommand command = CreateOrderCommand.builder()
            .customerId(1L)
            .items(List.of(new OrderItemRequest("P001", 2)))
            .build();
        
        // When
        OrderId orderId = orderService.createOrder(command);
        
        // Then
        Order order = orderRepository.findById(orderId.getValue()).orElseThrow();
        assertThat(order.getStatus()).isEqualTo(OrderStatus.CREATED);
        
        verify(eventPublisher).publish(argThat(event ->
            event instanceof OrderCreatedEvent &&
            ((OrderCreatedEvent) event).getOrderId().equals(orderId)
        ));
    }
    
    @Test
    void shouldReserveInventoryWhenOrderCreated() {
        // Given
        CreateOrderCommand command = CreateOrderCommand.builder()
            .customerId(1L)
            .items(List.of(new OrderItemRequest("P001", 2)))
            .build();
        
        // When
        OrderId orderId = orderService.createOrder(command);
        
        // Then
        verify(inventoryClient).reserve(orderId.getValue(), 
            argThat(items -> items.size() == 1));
    }
}

契约测试

契约测试验证服务间接口的兼容性。

// 消费者端契约测试
@ExtendWith(RestAssuredRestExtension.class)
class OrderServiceContractTest {
    
    @TestTemplate
    @ContextConfiguration(locations = "classpath:/consumer-context.xml")
    void consumeOrderCreatedEvent(RestDocumentationContextProvider provider) {
        RestAssured.given()
            .spec(RestAssuredRestDocumentation.documentationSpecification(provider))
        .when()
            .body(new ClassPathResource("order-created-event.json"))
            .post("/events")
        .then()
            .statusCode(200);
    }
}

// 提供者端契约测试
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class OrderProviderContractTest {
    
    @LocalServerPort
    private int port;
    
    @Test
    void verifyOrderCreatedEventSchema() {
        // 启动 Pact 验证服务器
        PactVerificationServer server = new PactVerificationServer(port);
        
        // 验证事件符合契约
        server.verify(new ClassPathResource("pact/order-events.json"));
    }
}

测试金字塔

        /\
       /  \  E2E 测试(少量)
      /    \  端到端业务流程
     /──────\
    /        \  集成测试(适量)
   /          \  服务间交互、数据库操作
  /────────────\
 /              \  单元测试(大量)
/                \  领域逻辑、值对象
──────────────────

DDD 测试的核心原则是领域层测试为主,上层测试验证集成和流程。