一篇文章带你了解单元测试如何实现

33 阅读8分钟

Mock

什么时候才需要Mock?

某个服务需要依赖其他的一些服务,这时候我们又不想要调用其他的一些服务来获取数据,所以直接通过mock模拟数据进行返回对应的数据。

mock解决的问题就是调用业务方法的时候,避免从外部依赖中查询数据,例如数据库、缓存这种,在自己内部设置模拟数据,当执行某些操作某些方法的时候将会调用mock内部的一些返回数据。

一般我们都是通过mock直接将那些本地需要以来的服务给抽离出来,然后再before里面定义该服务调用返回的数据,当调用真实服务时返回数据是模拟的数据

注解

注解功能使用场景
@Mock创建模拟对象模拟外部依赖(数据库、API等)
@InjectMocks自动注入依赖被测试类需要依赖注入时
@ExtendWith启用框架支持集成Mockito或Spring测试
@Spy部分模拟真实对象需要测试部分真实逻辑时
@ParameterizedTest参数化测试多组数据测试相同逻辑
// 真实的退保服务(有外部依赖)
public class RealSurrenderService {
    private PolicyRepository policyRepo; // 依赖数据库
    private PaymentService paymentService; // 依赖支付系统
    private EmailService emailService; // 依赖邮件服务
    
    public SurrenderResult surrenderPolicy(String policyId) {
        // 1. 从数据库查询保单
        InsurancePolicy policy = policyRepo.findById(policyId);
        if (policy == null) throw new RuntimeException("保单不存在");
        
        // 2. 调用支付系统退款
        boolean refundSuccess = paymentService.processRefund(policy.getUserId(), policy.getPremium());
        if (!refundSuccess) throw new RuntimeException("退款失败");
        
        // 3. 发送邮件通知
        emailService.sendSurrenderEmail(policy.getUserId());
        
        return new SurrenderResult(true, "退保成功");
    }
}

Mock的精确匹配机制

@Test
void testMockMatching() {
    // 设置Mock行为:只有当参数是"P001"时才返回mockPolicy
    when(policyRepo.findById("P001")).thenReturn(mockPolicy);
    
    //也不会直接去查询数据库
    InsurancePolicy result1 = policyRepo.findById("P001"); // 返回 mockPolicy ✅
    InsurancePolicy result2 = policyRepo.findById("P002"); // 返回 null ❌(因为没有设置)
    InsurancePolicy result3 = policyRepo.findById("任意其他值"); // 返回 null ❌
    
    assertSame(mockPolicy, result1); // 通过
    assertNull(result2); // 通过
    assertNull(result3); // 通过
}

参数不同时的行为

@Test
void testDifferentParameters() {
    InsurancePolicy policy1 = new InsurancePolicy("P001", "ACTIVE", 10000.0, 2);
    InsurancePolicy policy2 = new InsurancePolicy("P002", "EXPIRED", 5000.0, 1);
    
    // 只设置了P001的行为
    when(policyRepo.findById("P001")).thenReturn(policy1);
    // 注意:没有设置P002的行为!
    
    // 测试不同参数
    InsurancePolicy result1 = policyRepo.findById("P001"); // 返回 policy1 ✅
    InsurancePolicy result2 = policyRepo.findById("P002"); // 返回 null(默认值)
    InsurancePolicy result3 = policyRepo.findById("任意值"); // 返回 null
    
    assertNotNull(result1);
    assertNull(result2); // 因为没设置P002的mock行为
    assertNull(result3);
}

项目结构

正常项目中的结构

@Service
public class OrderService {
    // 依赖的外部服务
    @Autowired private UserService userService;      // 用户服务
    @Autowired private ProductService productService; // 商品服务  
    @Autowired private InventoryService inventoryService; // 库存服务
    @Autowired private PaymentService paymentService; // 支付服务
    
    public OrderResult createOrder(String userId, String productId, int quantity) {
        // 1. 验证用户是否存在(依赖用户服务)
        User user = userService.getUserById(userId);
        if (user == null) throw new UserNotFoundException();
        
        // 2. 获取商品信息(依赖商品服务)
        Product product = productService.getProductById(productId);
        if (product == null) throw new ProductNotFoundException();
        
        // 3. 检查库存(依赖库存服务)
        boolean inStock = inventoryService.checkInventory(productId, quantity);
        if (!inStock) throw new InsufficientInventoryException();
        
        // 4. 创建订单(业务逻辑)
        Order order = new Order(userId, productId, quantity, product.getPrice());
        
        // 5. 扣减库存(依赖库存服务)
        inventoryService.deductInventory(productId, quantity);
        
        // 6. 发送创建通知(可能依赖消息服务)
        notificationService.sendOrderCreated(userId, order.getId());
        
        return new OrderResult(true, "订单创建成功", order.getId());
    }
}

对应的单元测试结构

class OrderServiceTest {
    
    // 🔥 1. 声明要Mock的依赖服务
    @Mock private UserService userService;
    @Mock private ProductService productService;
    @Mock private InventoryService inventoryService;
    @Mock private PaymentService paymentService;
    @Mock private NotificationService notificationService;
    
    // 被测试的主服务
    private OrderService orderService;
    
    // 🔥 2. 在@BeforeEach中初始化Mock和设置默认行为
    @BeforeEach
    void setUp() {
        // 初始化Mock注解
        MockitoAnnotations.openMocks(this);
        
        // 创建被测试对象,注入Mock的依赖
        orderService = new OrderService(userService, productService, 
                                      inventoryService, paymentService, notificationService);
        
        // 🔥 3. 设置通用的Mock行为(所有测试用例共享的基础场景)
        setupCommonMockBehavior();
    }
    
    private void setupCommonMockBehavior() {
        // 模拟用户存在
        when(userService.getUserById("user123"))
            .thenReturn(new User("user123", "张三", "ACTIVE"));
        
        // 模拟商品存在
        when(productService.getProductById("product456"))
            .thenReturn(new Product("product456", "iPhone14", 5999.0));
        
        // 模拟库存充足
        when(inventoryService.checkInventory("product456", anyInt()))
            .thenReturn(true);
        
        // 模拟库存扣减成功
        when(inventoryService.deductInventory(anyString(), anyInt()))
            .thenReturn(true);
    }
    
    // 🔥 4. 具体的测试用例(基于通用Mock行为,覆盖特定场景)
    @Test
    void testCreateOrder_Success() {
        // 执行测试 - 使用@BeforeEach中设置的Mock数据
        OrderResult result = orderService.createOrder("user123", "product456", 2);
        
        // 验证结果
        assertTrue(result.isSuccess());
        assertEquals("订单创建成功", result.getMessage());
        
        // 验证交互:确认调用了相关服务
        verify(userService).getUserById("user123");
        verify(productService).getProductById("product456");
        verify(inventoryService).checkInventory("product456", 2);
        verify(inventoryService).deductInventory("product456", 2);
        verify(notificationService).sendOrderCreated("user123", anyString());
    }
    
    @Test
    void testCreateOrder_UserNotFound() {
        // 🔥 覆盖默认行为:模拟用户不存在
        when(userService.getUserById("non_exist_user"))
            .thenReturn(null);
        
        // 执行测试并验证异常
        assertThrows(UserNotFoundException.class, () -> {
            orderService.createOrder("non_exist_user", "product456", 1);
        });
        
        // 验证:用户不存在时,后续服务不应该被调用
        verify(productService, never()).getProductById(anyString());
        verify(inventoryService, never()).checkInventory(anyString(), anyInt());
    }
    
    @Test 
    void testCreateOrder_OutOfStock() {
        // 🔥 覆盖默认行为:模拟库存不足
        when(inventoryService.checkInventory("product456", 10))
            .thenReturn(false); // 要10件,但库存不足
        
        assertThrows(InsufficientInventoryException.class, () -> {
            orderService.createOrder("user123", "product456", 10);
        });
        
        // 验证:库存检查失败后,不应该扣减库存
        verify(inventoryService, never()).deductInventory(anyString(), anyInt());
    }
}

我们以退保服务为例

保单实体类

public class InsurancePolicy {
    private String policyId;
    private String policyholderId;
    private String status; 、、
    private double premium;
    private Date startDate;
    private Date surrenderDate;
    private Double surrenderValue;
    
    public InsurancePolicy(String policyId, String policyholderId, String status, 
                          double premium, Date startDate) {
        this.policyId = policyId;
        this.policyholderId = policyholderId;
        this.status = status;
        this.premium = premium;
        this.startDate = startDate;
    }
    
    public String getPolicyId() { return policyId; }
    public String getPolicyholderId() { return policyholderId; }
    public String getStatus() { return status; }
    public void setStatus(String status) { this.status = status; }
    public double getPremium() { return premium; }
    public Date getStartDate() { return startDate; }
    public Date getSurrenderDate() { return surrenderDate; }
    public void setSurrenderDate(Date surrenderDate) { this.surrenderDate = surrenderDate; }
    public Double getSurrenderValue() { return surrenderValue; }
    public void setSurrenderValue(Double surrenderValue) { this.surrenderValue = surrenderValue; }
}
}

退款和退保结果类

public class RefundResult {
    private boolean success;
    private String message;
    private String transactionId;
    
    public RefundResult(boolean success, String message, String transactionId) {
        this.success = success;
        this.message = message;
        this.transactionId = transactionId;
    }
    
    public boolean isSuccess() { return success; }
    public String getMessage() { return message; }
    public String getTransactionId() { return transactionId; }
}
​
public class SurrenderResult {
    private boolean success;
    private double surrenderValue;
    private String message;
    
    public SurrenderResult(boolean success, double surrenderValue, String message) {
        this.success = success;
        this.surrenderValue = surrenderValue;
        this.message = message;
    }
    
    public boolean isSuccess() { return success; }
    public double getSurrenderValue() { return surrenderValue; }
    public String getMessage() { return message; }
}
​
​

各个接口

// 保单仓储接口
public interface PolicyRepository {
    InsurancePolicy getPolicy(String policyId);
    void updatePolicy(InsurancePolicy policy);
}
​
// 财务服务接口
public interface FinancialService {
    RefundResult processRefund(String policyholderId, double amount);
}
​
// 通知服务接口
public interface NotificationService {
    void sendSurrenderNotification(String policyholderId, String policyId, double surrenderValue);
}

保险退保服务实现

// 服务实现
@Service
public class InsuranceSurrenderService {
    private final PolicyRepository policyRepository;
    private final FinancialService financialService;
    private final NotificationService notificationService;
    
    @Autowired
    public InsuranceSurrenderService(PolicyRepository policyRepository,
                                   FinancialService financialService,
                                   NotificationService notificationService) {
        this.policyRepository = policyRepository;
        this.financialService = financialService;
        this.notificationService = notificationService;
    }

主流程

 /**
     * 退保主流程
     */
    public SurrenderResult surrenderPolicy(String policyId, Date surrenderDate) {
        // 1. 获取保单信息
        InsurancePolicy policy = policyRepository.getPolicy(policyId);
        if (policy == null) {
            throw new IllegalArgumentException("保单不存在");
        }
        
        // 2. 验证保单状态
        if (!"ACTIVE".equals(policy.getStatus())) {
            throw new IllegalStateException("只有有效保单可以退保");
        }
        
        // 3. 计算退保金额
        double surrenderValue = calculateSurrenderValue(policy, surrenderDate);
        
        // 4. 调用财务系统退款
        RefundResult refundResult = financialService.processRefund(
            policy.getPolicyholderId(), surrenderValue);
        
        if (!refundResult.isSuccess()) {
            throw new RuntimeException("退款失败: " + refundResult.getMessage());
        }
        
        // 5. 更新保单状态
        policy.setStatus("SURRENDERED");
        policy.setSurrenderDate(surrenderDate);
        policy.setSurrenderValue(surrenderValue);
        
        policyRepository.updatePolicy(policy);
        
        // 6. 发送通知
        notificationService.sendSurrenderNotification(
            policy.getPolicyholderId(), policyId, surrenderValue);
        
        return new SurrenderResult(true, surrenderValue, "退保成功");
    }
    
    /**
     * 计算退保金额
     */
    private double calculateSurrenderValue(InsurancePolicy policy, Date surrenderDate) {
        long yearsHeld = getYearsHeld(policy.getStartDate(), surrenderDate);
        double surrenderRate = 0.3; // 默认退保率
        
        if (yearsHeld >= 5) {
            surrenderRate = 0.8;
        } else if (yearsHeld >= 3) {
            surrenderRate = 0.6;
        } else if (yearsHeld >= 1) {
            surrenderRate = 0.4;
        }
        
        return policy.getPremium() * yearsHeld * surrenderRate;
    }
    
    /**
     * 计算保单持有年限
     */
    private long getYearsHeld(Date startDate, Date endDate) {
        long diffInMillies = Math.abs(endDate.getTime() - startDate.getTime());
        return (long) (diffInMillies / (1000.0 * 60 * 60 * 24 * 365.25));
    }

单元测试代码

 // 🔥 @BeforeEach:测试前的准备工作
 // 🔥 @Test:测试用例1 - 正常退保
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
​
import java.text.SimpleDateFormat;
import java.util.Date;
​
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyDouble;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;
​
@ExtendWith(MockitoExtension.class)
class InsuranceSurrenderServiceTest {
    
    @Mock
    private PolicyRepository policyRepository;
    
    @Mock
    private FinancialService financialService;
    
    @Mock
    private NotificationService notificationService;
    
    @InjectMocks
    private InsuranceSurrenderService surrenderService;
    
    private InsurancePolicy activePolicy;
    private Date surrenderDate;
    
    @BeforeEach
    void setUp() throws Exception {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        Date startDate = sdf.parse("2020-01-01");
        surrenderDate = sdf.parse("2023-06-15");
        
        activePolicy = new InsurancePolicy("POL001", "USER001", "ACTIVE", 5000.0, startDate);
    }
    
    @Test
    void testSurrenderPolicy_Success() {
        // 准备Mock行为
        when(policyRepository.getPolicy("POL001")).thenReturn(activePolicy);
        when(financialService.processRefund(eq("USER001"), anyDouble()))
            .thenReturn(new RefundResult(true, "退款成功", "TX123456"));
        
        // 执行测试
        SurrenderResult result = surrenderService.surrenderPolicy("POL001", surrenderDate);
        
        // 验证结果
        assertTrue(result.isSuccess());
        assertTrue(result.getSurrenderValue() > 0);
        assertEquals("退保成功", result.getMessage());
        
        // 验证Mock交互
        verify(policyRepository).getPolicy("POL001");
        verify(financialService).processRefund("USER001", result.getSurrenderValue());
        verify(policyRepository).updatePolicy(activePolicy);
        verify(notificationService).sendSurrenderNotification("USER001", "POL001", result.getSurrenderValue());
        
        // 验证保单状态更新
        assertEquals("SURRENDERED", activePolicy.getStatus());
        assertEquals(surrenderDate, activePolicy.getSurrenderDate());
        assertEquals(result.getSurrenderValue(), activePolicy.getSurrenderValue());
    }
    
    @Test
    void testSurrenderPolicy_PolicyNotFound() {
        // 准备Mock行为
        when(policyRepository.getPolicy("NON_EXISTENT")).thenReturn(null);
        
        // 执行测试并验证异常
        IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, 
            () -> surrenderService.surrenderPolicy("NON_EXISTENT", surrenderDate));
        
        assertEquals("保单不存在", exception.getMessage());
        
        // 验证没有后续的Mock调用
        verify(financialService, never()).processRefund(any(), anyDouble());
        verify(policyRepository, never()).updatePolicy(any());
        verify(notificationService, never()).sendSurrenderNotification(any(), any(), anyDouble());
    }
    
    @Test
    void testSurrenderPolicy_PolicyNotActive() {
        // 准备非活跃保单
        InsurancePolicy expiredPolicy = new InsurancePolicy("POL002", "USER002", "EXPIRED", 5000.0, activePolicy.getStartDate());
        when(policyRepository.getPolicy("POL002")).thenReturn(expiredPolicy);
        
        // 执行测试并验证异常
        IllegalStateException exception = assertThrows(IllegalStateException.class, 
            () -> surrenderService.surrenderPolicy("POL002", surrenderDate));
        
        assertEquals("只有有效保单可以退保", exception.getMessage());
        
        // 验证没有后续的Mock调用
        verify(financialService, never()).processRefund(any(), anyDouble());
        verify(policyRepository, never()).updatePolicy(any());
        verify(notificationService, never()).sendSurrenderNotification(any(), any(), anyDouble());
    }
    
    @Test
    void testSurrenderPolicy_RefundFailed() {
        // 准备Mock行为
        when(policyRepository.getPolicy("POL001")).thenReturn(activePolicy);
        when(financialService.processRefund(eq("USER001"), anyDouble()))
            .thenReturn(new RefundResult(false, "余额不足", null));
        
        // 执行测试并验证异常
        RuntimeException exception = assertThrows(RuntimeException.class, 
            () -> surrenderService.surrenderPolicy("POL001", surrenderDate));
        
        assertEquals("退款失败: 余额不足", exception.getMessage());
        
        // 验证没有更新保单和发送通知
        verify(policyRepository, never()).updatePolicy(any());
        verify(notificationService, never()).sendSurrenderNotification(any(), any(), anyDouble());
    }
    
    @Test
    void testCalculateSurrenderValue() throws Exception {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        
        // 测试不同持有年限的退保金额计算
        Date startDate = sdf.parse("2020-01-01");
        
        // 持有1年
        Date surrenderDate1 = sdf.parse("2021-01-01");
        InsurancePolicy policy1 = new InsurancePolicy("POL003", "USER003", "ACTIVE", 5000.0, startDate);
        double value1 = surrenderService.calculateSurrenderValue(policy1, surrenderDate1);
        assertEquals(2000.0, value1, 0.01); // 5000 * 1 * 0.4
        
        // 持有3年
        Date surrenderDate3 = sdf.parse("2023-01-01");
        InsurancePolicy policy3 = new InsurancePolicy("POL004", "USER004", "ACTIVE", 5000.0, startDate);
        double value3 = surrenderService.calculateSurrenderValue(policy3, surrenderDate3);
        assertEquals(9000.0, value3, 0.01); // 5000 * 3 * 0.6
        
        // 持有5年
        Date surrenderDate5 = sdf.parse("2025-01-01");
        InsurancePolicy policy5 = new InsurancePolicy("POL005", "USER005", "ACTIVE", 5000.0, startDate);
        double value5 = surrenderService.calculateSurrenderValue(policy5, surrenderDate5);
        assertEquals(20000.0, value5, 0.01); // 5000 * 5 * 0.8
    }
}

单元测试

image-20251125185029669.png

image-20251125185047760.png

image-20251125185201965.png