# Mock 与 Stub 的使用

URL: https://caijiao.org/ddd/chapter7/mock-stub
Source: docs/ddd/chapter7/mock-stub.md
Description: 介绍 Mock 和 Stub 在领域驱动设计中的使用，帮助开发者理解如何使用它们来模拟和隔离外部依赖，提高测试的准确性和可维护性。

在领域驱动设计（DDD）中，Mock 和 Stub 是常用的测试替身（Test Double）工具，它们在单元测试和集成测试中扮演着重要角色。通过模拟外部依赖，Mock 和 Stub 可以帮助开发者隔离待测试的业务逻辑，确保测试环境的可控性与稳定性。尽管它们的功能相似，但在用途和实现上有所区别。

## Mock 和 Stub 的区别

- **Stub**：通常用于提供固定的返回值，用于模拟外部依赖的行为，帮助测试目标代码的不同执行路径。Stub 是一种简单的模拟方法，主要用于返回期望的结果，通常不会记录交互。
- **Mock**：不仅用于提供固定的返回值，还可以记录方法调用的交互信息（例如调用次数、参数等），从而对交互进行验证。Mock 主要用于验证某个方法是否按预期被调用，并且是否满足特定的交互规则。

## 使用 Stub

Stub 通常用于模拟外部系统的响应，特别是当外部系统可能不可用或不希望进行复杂交互时。Stub 的主要作用是控制外部依赖的行为，让测试集中在领域模型本身。

### 示例：使用 Stub 模拟数据库访问

假设我们有一个 `OrderService` 类，其中需要从数据库中查询订单信息。为了避免在测试中依赖数据库，我们可以使用 Stub 来模拟数据库访问。

```java
public class OrderService {
    private OrderRepository orderRepository;

    public OrderService(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }

    public Order getOrderById(String orderId) {
        return orderRepository.findById(orderId);
    }
}

public interface OrderRepository {
    Order findById(String orderId);
}

public class StubOrderRepository implements OrderRepository {
    @Override
    public Order findById(String orderId) {
        // 返回一个假数据
        return new Order(orderId, 100.0);
    }
}
```

在上面的示例中，`StubOrderRepository` 提供了一个简单的实现，返回固定的订单数据。通过这种方式，`OrderService` 的测试不依赖于真实的数据库操作，而是通过 Stub 来模拟外部依赖。

### Stub 的优缺点

- **优点**：
  - 简单易用，适合模拟静态的、无复杂行为的依赖。
  - 使得单元测试不依赖外部服务或数据库。
- **缺点**：
  - 无法验证外部依赖是否按预期被调用，仅能返回固定的结果。
  - 不能用于验证交互或调用次数等。

## 使用 Mock

Mock 更适用于需要验证方法调用及其交互的场景，尤其是在领域驱动设计中，当业务逻辑需要依赖于外部服务或系统时，Mock 可以帮助开发者确保交互的正确性。

### 示例：使用 Mock 验证方法调用

假设我们有一个 `NotificationService` 类，需要发送邮件通知给客户。我们希望验证 `sendEmail` 方法是否被正确调用。

```java
public class NotificationService {
    private EmailSender emailSender;

    public NotificationService(EmailSender emailSender) {
        this.emailSender = emailSender;
    }

    public void sendOrderConfirmation(Order order) {
        emailSender.sendEmail(order.getCustomer().getEmail(), "Order Confirmation", "Your order has been confirmed!");
    }
}

public interface EmailSender {
    void sendEmail(String to, String subject, String body);
}
```

使用 Mock 对 `EmailSender` 进行模拟，验证 `sendEmail` 是否按预期被调用。

```java
import static org.mockito.Mockito.*;

public class NotificationServiceTest {

    @Test
    public void testSendOrderConfirmation() {
        // 创建一个 Mock 对象
        EmailSender mockEmailSender = mock(EmailSender.class);

        // 创建 NotificationService 实例
        NotificationService notificationService = new NotificationService(mockEmailSender);

        // 创建订单对象
        Order order = new Order("123", new Customer("john@example.com"));

        // 调用方法
        notificationService.sendOrderConfirmation(order);

        // 验证 sendEmail 方法是否被调用
        verify(mockEmailSender).sendEmail("john@example.com", "Order Confirmation", "Your order has been confirmed!");
    }
}
```

在上面的示例中，`mock(EmailSender.class)` 创建了一个 Mock 对象，`verify(mockEmailSender)` 用来验证 `sendEmail` 方法是否按预期被调用。这使得我们可以确保 `NotificationService` 类在发送订单确认邮件时，正确地调用了 `EmailSender`。

### Mock 的优缺点

- **优点**：
  - 可以验证外部依赖的方法是否按预期被调用。
  - 支持验证方法调用次数、参数和交互。
  - 更适合验证复杂的行为和业务逻辑。
- **缺点**：
  - 相较于 Stub，Mock 配置较为复杂。
  - 如果测试不当，可能导致测试用例与实现细节耦合较高，难以维护。

## 总结

Mock 和 Stub 是两种常见的测试替身工具，它们在领域驱动设计中的使用有助于隔离外部依赖并确保领域模型的行为得到验证。Stub 适合模拟静态的外部依赖，而 Mock 更适合验证方法调用及交互。选择哪种工具取决于测试的需求：如果只关心结果，Stub 是一个简单的选择；如果需要验证方法调用和交互，Mock 是更好的选择。

在实际开发中，合理使用 Mock 和 Stub 可以显著提高单元测试的效率和质量，同时确保领域模型的设计符合业务需求。
