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
}
}