当前位置: 首页 > news >正文

Java 软件测试(三):Mockito打桩与静态方法模拟解析

写单元测试的时候,
经常会遇到一个问题:怎么处理那些复杂的依赖关系?比如数据库调用、网络请求,或者一些第三方服务。

Mockito就是为了解决这个问题而生的。它提供了两种核心的模拟技术:打桩(Stubbing)和Mock静态方法。这两个技术看起来相似,但实际应用场景却大不相同。

1. 打桩技术详解

1.1 什么是打桩

打桩说白了就是给Mock对象"预设台词"。你告诉它:当有人调用某个方法时,你就返回这个结果。

这样做的好处是什么?测试的时候不用真的去调用数据库或者网络服务,直接用预设的结果就行了。

// 创建一个假的用户仓库
UserRepository userRepository = mock(UserRepository.class);// 给它设定行为:当查询ID为1的用户时,返回张三
when(userRepository.findById(1L)).thenReturn(new User(1L, "张三"));// 现在测试用户服务
User user = userService.getUserById(1L);
assertEquals("张三", user.getName());

1.2 打桩的高级用法

有时候你需要模拟更复杂的场景。比如网络延迟、多次调用返回不同结果,甚至是抛异常。

模拟网络延迟:

@Test
void testNetworkDelay() {PaymentService paymentService = mock(PaymentService.class);// 模拟支付接口响应慢doAnswer(invocation -> {Thread.sleep(2000); // 延迟2秒return new PaymentResult(true, "支付成功");}).when(paymentService).processPayment(any());OrderService orderService = new OrderService(paymentService);long startTime = System.currentTimeMillis();orderService.createOrder(new OrderRequest());long duration = System.currentTimeMillis() - startTime;assertTrue(duration >= 2000); // 验证确实等待了2秒
}

模拟多次调用的不同结果:

@Test
void testRetryMechanism() {ExternalApiService apiService = mock(ExternalApiService.class);// 第一次调用失败,第二次成功when(apiService.callApi()).thenThrow(new NetworkException("网络超时")).thenReturn(new ApiResponse("success"));RetryService retryService = new RetryService(apiService);ApiResponse response = retryService.callWithRetry();assertEquals("success", response.getStatus());verify(apiService, times(2)).callApi(); // 验证确实调用了2次
}

2. Mock静态方法的应用

2.1 为什么需要Mock静态方法

有些代码依赖静态方法,比如System.currentTimeMillis()UUID.randomUUID(),或者一些工具类的静态方法。这些方法很难控制,测试起来就比较麻烦。

Mockito 3.4.0之后提供了Mock静态方法的功能,让这类测试变得简单多了。

@Test
void testTimeBasedDiscount() {try (MockedStatic<LocalDateTime> timeMock = mockStatic(LocalDateTime.class)) {// 假设现在是黑色星期五LocalDateTime blackFriday = LocalDateTime.of(2023, 11, 24, 10, 0);timeMock.when(LocalDateTime::now).thenReturn(blackFriday);DiscountService discountService = new DiscountService();double discount = discountService.getCurrentDiscount();assertEquals(0.5, discount); // 黑色星期五5折}
}

2.2 Mock静态方法的实际场景

模拟文件操作:

@Test
void testFileProcessing() {try (MockedStatic<Files> filesMock = mockStatic(Files.class)) {// 模拟文件存在Path testPath = Paths.get("/test/file.txt");filesMock.when(() -> Files.exists(testPath)).thenReturn(true);filesMock.when(() -> Files.readAllLines(testPath)).thenReturn(Arrays.asList("line1", "line2", "line3"));FileProcessor processor = new FileProcessor();List<String> result = processor.processFile("/test/file.txt");assertEquals(3, result.size());filesMock.verify(() -> Files.exists(testPath));}
}

模拟日志记录:

@Test
void testErrorLogging() {try (MockedStatic<LoggerFactory> loggerMock = mockStatic(LoggerFactory.class)) {Logger mockLogger = mock(Logger.class);loggerMock.when(() -> LoggerFactory.getLogger(any(Class.class))).thenReturn(mockLogger);ErrorHandler errorHandler = new ErrorHandler();errorHandler.handleError(new RuntimeException("测试异常"));// 验证错误日志被记录verify(mockLogger).error(contains("测试异常"));}
}

3. 两种技术的区别与选择

3.1 核心差异

打桩和Mock静态方法最大的区别在于作用范围。

打桩只影响你创建的那个Mock对象,其他地方的调用不受影响。而Mock静态方法是全局的,会影响所有对该静态方法的调用。

// 打桩 - 只影响这个mock对象
UserService mockUserService = mock(UserService.class);
when(mockUserService.getUser(1L)).thenReturn(testUser);// Mock静态方法 - 影响所有对LocalDateTime.now()的调用
try (MockedStatic<LocalDateTime> timeMock = mockStatic(LocalDateTime.class)) {timeMock.when(LocalDateTime::now).thenReturn(fixedTime);// 在这个try块内,所有LocalDateTime.now()都返回fixedTime
}

3.2 生命周期管理

这是另一个重要区别。Mock对象的生命周期跟着测试方法走,测试结束就销毁了。

但Mock静态方法需要手动管理。必须用try-with-resources语句,或者手动调用close()方法。否则会影响其他测试。

@Test
void badExample() {MockedStatic<UUID> uuidMock = mockStatic(UUID.class);uuidMock.when(UUID::randomUUID).thenReturn(fixedUuid);// 忘记关闭,会影响其他测试!
}@Test
void goodExample() {try (MockedStatic<UUID> uuidMock = mockStatic(UUID.class)) {uuidMock.when(UUID::randomUUID).thenReturn(fixedUuid);// 自动关闭,不会影响其他测试}
}

4. 实战应用场景

4.1 电商订单处理

假设你在开发一个电商系统的订单处理功能。这个功能涉及库存检查、支付处理、订单状态更新等多个步骤。

@ExtendWith(MockitoExtension.class)
class OrderProcessorTest {@Mockprivate InventoryService inventoryService;@Mockprivate PaymentService paymentService;@Mockprivate NotificationService notificationService;@InjectMocksprivate OrderProcessor orderProcessor;@Testvoid shouldProcessOrderSuccessfully() {// 模拟库存充足when(inventoryService.checkStock("iPhone15", 1)).thenReturn(true);// 模拟支付成功PaymentResult successResult = new PaymentResult(true, "TXN123");when(paymentService.charge(any(PaymentRequest.class))).thenReturn(successResult);OrderRequest request = new OrderRequest("iPhone15", 1, 8999.0);OrderResult result = orderProcessor.processOrder(request);assertTrue(result.isSuccess());verify(inventoryService).reserveStock("iPhone15", 1);verify(notificationService).sendOrderConfirmation(any());}@Testvoid shouldHandlePaymentFailure() {when(inventoryService.checkStock("iPhone15", 1)).thenReturn(true);// 模拟支付失败PaymentResult failResult = new PaymentResult(false, "余额不足");when(paymentService.charge(any())).thenReturn(failResult);OrderRequest request = new OrderRequest("iPhone15", 1, 8999.0);OrderResult result = orderProcessor.processOrder(request);assertFalse(result.isSuccess());assertEquals("支付失败:余额不足", result.getErrorMessage());// 确保库存被释放verify(inventoryService).releaseStock("iPhone15", 1);}
}

4.2 定时任务处理

很多业务场景需要根据时间来执行不同的逻辑。比如每天凌晨的数据统计、节假日的特殊处理等。

@Test
void testDailyReportGeneration() {try (MockedStatic<LocalDateTime> timeMock = mockStatic(LocalDateTime.class)) {// 模拟是工作日的上午9点LocalDateTime workdayMorning = LocalDateTime.of(2023, 10, 16, 9, 0); // 周一timeMock.when(LocalDateTime::now).thenReturn(workdayMorning);ReportService reportService = new ReportService();boolean shouldGenerate = reportService.shouldGenerateDailyReport();assertTrue(shouldGenerate);}
}@Test
void testWeekendSkip() {try (MockedStatic<LocalDateTime> timeMock = mockStatic(LocalDateTime.class)) {// 模拟是周末LocalDateTime weekend = LocalDateTime.of(2023, 10, 15, 9, 0); // 周日timeMock.when(LocalDateTime::now).thenReturn(weekend);ReportService reportService = new ReportService();boolean shouldGenerate = reportService.shouldGenerateDailyReport();assertFalse(shouldGenerate);}
}

5. 总计

什么时候用打桩

打桩适合处理那些你能控制的依赖对象。比如DAO层、Service层的依赖,或者一些业务组件。

这些对象通常是通过依赖注入传入的,你可以很容易地用Mock对象替换它们。

什么时候用Mock静态方法

当你遇到以下情况时,考虑使用Mock静态方法:

  • 代码依赖系统时间(LocalDateTime.now()System.currentTimeMillis()
  • 使用了工具类的静态方法(UUID.randomUUID()Files.readAllLines()
  • 调用了第三方库的静态API
  • 需要模拟单例对象的行为

注意事项

避免过度使用Mock:

不是所有依赖都需要Mock。对于简单的值对象、数据传输对象,直接创建真实对象往往更简单。

// 不需要Mock的情况
User user = new User("张三", "zhangsan@example.com");
Address address = new Address("北京市", "朝阳区");// 需要Mock的情况
UserRepository userRepository = mock(UserRepository.class);
EmailService emailService = mock(EmailService.class);

保持测试的独立性:

每个测试方法都应该是独立的,不应该依赖其他测试的执行结果。特别是使用Mock静态方法时,一定要确保正确清理。

测试要有意义:

不要为了测试而测试。每个测试都应该验证一个明确的业务逻辑或者边界条件。

// 有意义的测试
@Test
void shouldRejectOrderWhenStockInsufficient() {when(inventoryService.checkStock("iPhone15", 10)).thenReturn(false);OrderRequest request = new OrderRequest("iPhone15", 10, 89990.0);assertThrows(InsufficientStockException.class, () -> {orderProcessor.processOrder(request);});
}

Mockito的打桩和Mock静态方法是单元测试中的两个重要工具。掌握它们的使用方法和适用场景,能让你的测试代码更加健壮和可维护。记住,好的测试不仅能发现bug,还能作为代码的活文档,帮助其他开发者理解业务逻辑。


文章转载自:

http://voCpDvVh.hLwzd.cn
http://V3tObkxJ.hLwzd.cn
http://CrdJMm51.hLwzd.cn
http://DDPcqsPL.hLwzd.cn
http://nkwSh1Es.hLwzd.cn
http://pTSZ7gxT.hLwzd.cn
http://KxoYu80L.hLwzd.cn
http://snbvqoFG.hLwzd.cn
http://G9C60wfr.hLwzd.cn
http://16kFXA6g.hLwzd.cn
http://hVrG9ef5.hLwzd.cn
http://OZXmK4vF.hLwzd.cn
http://eM2qRHaX.hLwzd.cn
http://dyCtNADm.hLwzd.cn
http://LbFAwXfH.hLwzd.cn
http://NKyWLynd.hLwzd.cn
http://4CcPSSQF.hLwzd.cn
http://NYPFYYsW.hLwzd.cn
http://3Al5pIX4.hLwzd.cn
http://lpbprBxg.hLwzd.cn
http://1vTx6twF.hLwzd.cn
http://4O5lXYS2.hLwzd.cn
http://5iZRH3q1.hLwzd.cn
http://wEwOyNGd.hLwzd.cn
http://2AuxDBQB.hLwzd.cn
http://4tvm4OVr.hLwzd.cn
http://7UezYXyv.hLwzd.cn
http://Zh6Zbwan.hLwzd.cn
http://UCDZaMoh.hLwzd.cn
http://MRQu2UNK.hLwzd.cn
http://www.dtcms.com/a/377990.html

相关文章:

  • 大数据与AI:一场“数据盛宴”与“智能大脑”的奇妙邂逅
  • 前端学习之后端java小白(四)之数据库设计
  • 构建高效协作的桥梁:前后端衔接实践与接口文档规范详解
  • 基于 Vue+SQLite3开发吉他谱推荐网站
  • Skynet火焰图swt搭建
  • 临床数据挖掘与分析:利用GPU加速Pandas和Scikit-learn处理大规模数据集
  • InfoSecWarrior CTF 2020: 01靶场渗透
  • SciKit-Learn 全面分析分类任务 wine 葡萄酒数据集
  • JMeter的安装部署
  • Lua语言基础笔记
  • Django的session机制
  • 从 @Component 到 @Builder:深度拆解 ArkTS 声明式 UI 与 @ohos.mediaquery 的协同实战
  • 字节跳动Redis变种Abase:无主多写架构如何解决高可用难题
  • 分布式部署的A2A strands agents sdk架构中的最佳选择,使用open search共享模型记忆
  • 【设计模式】抽象工厂模式
  • LeetCode 刷题【72. 编辑距离】
  • gitlab流水线与k8s集群的联通
  • 关于神经网络中回归的概念
  • 前后端接口调试提效:Postman + Mock Server 的工作流
  • Cesium---1.133版本不修改源码支持arcgis MapServer 4490切片
  • express 框架基础和 EJS 模板
  • 多楼层室内定位可视化 Demo(A*路径避障)
  • python将pdf转txt,并切割ai
  • 可视化图解算法60: 矩阵最长递增路径
  • 4、幽络源微服务项目实战:后端公共模块创建与引入多租户模块
  • 用Next.js 构建一个简单的 CRUD 应用:集成 API 路由和数据获取
  • 如何通过url打开本地文件文件夹
  • Swagger隐藏入参中属性字段
  • JavaEE--8.网络编程
  • linux系统搭建nacos集群,并通过nginx实现负载均衡