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

简介: 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 {
   

    @Mock
    private InventoryService inventoryService;

    @Mock
    private PaymentService paymentService;

    @Mock
    private NotificationService notificationService;

    @InjectMocks
    private OrderProcessor orderProcessor;

    @Test
    void 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());
    }

    @Test
    void 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,还能作为代码的活文档,帮助其他开发者理解业务逻辑。

目录
相关文章
|
1月前
|
缓存 人工智能 自然语言处理
阿里云Qwen3.7-Max全面评测:Agent智能体能力、计费方案与落地场景说明
2026年,AI行业正式迈入智能体(Agent)规模化落地的新阶段,能否支撑长周期自主任务、控制算力调用成本,成为企业与开发者选择大模型的两大核心标准。阿里云百炼平台重磅推出**Qwen3.7-Max**,作为通义千问系列面向智能体时代的旗舰大模型,该产品彻底突破传统对话模型的能力边界,主打长周期自主执行、全栈编程、办公自动化三大核心能力,同时搭配限时五折优惠与海量免费Token额度,大幅降低AI应用落地门槛。本文结合官方基准测试数据、功能特性、应用场景、调用方式、计费规则以及MCP集成方案,全方位解读Qwen3.7-Max,帮助个人开发者、初创团队、中大型企业全面了解这款旗舰模型
785 1
|
1月前
|
监控 Java 测试技术
Java 软件测试(二):Mockito与JUnit 5应用
Java 软件测试(二):Mockito与JUnit 5应用
107 0
|
1月前
|
运维 Java 测试技术
程序员必备的十大技能(进阶版)之性能调优与故障排查(四)
教程来源 https://aescc.cn/ 本节系统介绍Java应用性能分析与稳定性保障核心实践:涵盖Arthas诊断、火焰图性能剖析、全链路压测方法论及JMeter实战、混沌工程故障注入(ChaosBlade),并复盘Full GC与数据库死锁两大典型故障案例,助力高效定位与解决线上性能问题。
|
29天前
|
Java 测试技术 数据库连接
Java 软件测试(四):Mockito提升代码覆盖率的实用技巧
Java 软件测试(四):Mockito提升代码覆盖率的实用技巧
77 1
|
1月前
|
人工智能 运维 数据安全/隐私保护
2026年阿里云通义千问Qwen3.7-Plus全解析:功能、优势与618订阅方案
随着人工智能技术全面融入办公、创作、研发、教育等各行各业,大模型已经从小众技术产品转变为大众日常工具。不同定位的大模型有着明确的使用分层:旗舰模型综合性能顶尖,但调用成本高昂,仅适合核心复杂业务;轻量化模型价格低廉,却难以应对中等难度的推理、创作与代码任务。在这样的市场格局下,**通义千问Qwen3.7-Plus**作为阿里云通义千问3.7系列的中端主力模型应运而生。该模型依托阿里云百炼MaaS平台对外提供服务,兼顾综合性能与使用成本,平衡了能力、稳定性与性价比,成为个人用户、自由职业者、小型团队以及中小微企业的主流选择。2026年618大促期间,阿里云百炼针对Qwen3.7-Plus推出按量
832 1
|
1月前
|
存储 缓存 自然语言处理
轻量级RAG与SKILL架构深度融合:专属知识库驱动智能体精准知识匹配应用实践.138
本文提出“RAG轻量化+SKILL技能架构”融合方案,以“一技能一知识库”替代传统大一统向量库,解决检索干扰、维护困难、耦合度高等痛点;通过技能自治、知识专属、精准路由,实现低成本、高精度、易迭代的业务知识落地。
273 2
|
1月前
|
存储 人工智能 Java
2026年AgentScope Java 2.0详解:架构升级、分布式企业级智能体底座全解析
在AI智能体规模化落地的当下,原型验证阶段的简单运行已经无法满足企业生产环境的严苛要求。多数企业在部署AI智能体时,普遍遭遇分布式扩展困难、多租户数据隔离缺失、长期运行稳定性差、权限管控薄弱、模型调用易中断等一系列工程化难题。2026年6月,阿里正式推出**AgentScope Java 2.0**版本,作为AgentScope多语言体系的重要升级产物,该版本面向JVM生态深度优化,聚焦企业级生产场景,将分布式部署、多租户隔离、安全权限、容错机制等能力内化为框架原生特性,真正实现了从原型Demo到工业级智能体底座的跨越。本文结合框架架构、核心组件、关键能力、部署模式、扩展机制以及落地场景展开全
863 0
|
1月前
|
人工智能 JavaScript 前端开发
Open Design:让你的 Coding Agent 变身设计引擎,本地优先的开源 Claude Design 替代品
Open Design 是开源本地化设计智能体,让 Claude/Cursor 等 Coding Agent 直接生成品牌一致的原型、MP4动效、PPTX演示稿与视觉资产。100+技能×150套DESIGN.md设计系统×261插件×21 Agent全适配,数据不出本机。
|
1月前
|
人工智能 JavaScript 定位技术
CodeGraph+Hermes Agent组合部署构建代码地图教程:代码智能协同实操指南
在2026年编程AI工具高速发展的当下,各类编程Agent已经成为开发者日常工作的重要助手,无论是代码编写、问题排查、项目重构还是接口调试,AI智能体都能大幅降低人工成本。但在处理中大型代码仓库时,传统编程Agent普遍存在明显短板:需要反复调用文件读取、检索、目录查看等工具,不断遍历项目文件梳理代码结构,不仅消耗大量Token资源,还会拉长任务执行时长,工作效率大打降。而**CodeGraph**的出现完美解决了这一行业痛点,它依托AST语法树与本地知识图谱技术,提前为代码库构建结构化“代码地图”,让编程Agent无需重复探索项目结构。结合当下热门的Hermes Agent智能体框架,二者形
331 0
|
1月前
|
存储 Java 测试技术
Java 软件测试(一)核心概念与实例一览
Java 软件测试(一)核心概念与实例一览
101 0