Java 软件测试(四):Mockito提升代码覆盖率的实用技巧

简介: Java 软件测试(四):Mockito提升代码覆盖率的实用技巧

写单元测试的时候,经常会遇到一个问题:覆盖率总是上不去。明明写了很多测试,但还是有很多代码分支没有被执行到。

其实这个问题很常见,特别是在处理复杂业务逻辑的时候。代码覆盖率不仅仅是一个数字,它反映了你的测试到底测了多少代码。

1. 代码覆盖率到底有什么用

覆盖率的真正意义

代码覆盖率说白了就是告诉你:测试代码执行了多少源代码。包括代码行、分支、路径这些维度。

高覆盖率确实能帮你发现一些潜在的bug,让代码质量更好。但是要注意一点:覆盖率高不等于测试质量高。

有些人为了追求100%覆盖率,写了一堆没意义的测试。这样做其实是本末倒置了。

覆盖率的局限性

覆盖率只能告诉你哪些代码被执行了,但不能告诉你测试是否有效。比如你可能执行了某个方法,但没有验证返回值是否正确。

所以在关注覆盖率的同时,更要关注测试的有效性和合理性。

2. Mockito在提升覆盖率中的作用

隔离依赖的威力

写测试最头疼的就是各种依赖。数据库、网络请求、第三方服务,这些东西都会让测试变得复杂。

Mockito的作用就是帮你把这些依赖都"假装"掉。通过创建Mock对象,你可以专注于测试核心逻辑,不用担心外部服务的影响。

比如测试用户登录功能,你不需要真的去连数据库,只需要Mock一个UserRepository就行了。

模拟各种复杂场景

现实中有很多场景很难重现。比如网络超时、数据库连接失败、第三方服务返回异常数据等等。

用Mockito可以轻松模拟这些情况。你可以让Mock对象抛出异常,返回特定的值,甚至模拟延迟响应。

这样就能覆盖那些平时很难测试到的代码分支了。

保证测试的独立性

每个测试用例都应该是独立的,不应该依赖其他测试的结果。Mockito让你可以精确控制依赖的行为,确保每个测试都在一个干净的环境中运行。

这样不仅能提高覆盖率的精确度,还能让测试更加稳定可靠。

3. 提升覆盖率的实战技巧

3.1 全面打桩策略

打桩就是给Mock对象"预设台词"。Mockito提供了很多打桩的方法,合理使用这些方法能让你覆盖更多的代码分支。

多态返回是一个很有用的技巧。同一个方法可以根据不同的参数返回不同的结果,这样就能测试不同的执行路径了。

public class EmailService {
   
    public String sendEmail(String to, String content) {
   
        if (to.endsWith("@work.com")) {
   
            return sendWorkEmail(content);
        } else {
   
            return sendPersonalEmail(content);
        }
    }

    private String sendWorkEmail(String content) {
   
        // 发送工作邮件的逻辑
        return "Work email sent: " + content;
    }

    private String sendPersonalEmail(String content) {
   
        // 发送个人邮件的逻辑
        return "Personal email sent: " + content;
    }
}

@Test
void testSendEmailDifferentTypes() {
   
    EmailService emailService = new EmailService();

    // 测试工作邮箱分支
    String workResult = emailService.sendEmail("user@work.com", "工作内容");
    assertTrue(workResult.contains("Work email sent"));

    // 测试个人邮箱分支
    String personalResult = emailService.sendEmail("user@gmail.com", "个人内容");
    assertTrue(personalResult.contains("Personal email sent"));
}

条件打桩可以让你模拟更复杂的场景。比如根据不同的输入参数返回不同的结果,或者在特定条件下抛出异常。

@Test
void testEmailServiceWithMock() {
   
    EmailService emailService = mock(EmailService.class);

    // 工作邮箱返回成功
    when(emailService.sendEmail(contains("@work.com"), anyString()))
        .thenReturn("工作邮件发送成功");

    // 个人邮箱返回成功
    when(emailService.sendEmail(not(contains("@work.com")), anyString()))
        .thenReturn("个人邮件发送成功");

    // 空邮箱抛出异常
    when(emailService.sendEmail(eq(""), anyString()))
        .thenThrow(new IllegalArgumentException("邮箱不能为空"));

    // 测试各种情况
    assertEquals("工作邮件发送成功", emailService.sendEmail("test@work.com", "内容"));
    assertEquals("个人邮件发送成功", emailService.sendEmail("test@gmail.com", "内容"));

    assertThrows(IllegalArgumentException.class, () -> {
   
        emailService.sendEmail("", "内容");
    });
}

3.2 深度模拟与验证

有时候你不想完全Mock一个对象,只想Mock其中的某些方法。这时候可以用@Spy或者Mockito.spy()

Spy对象会保留原有的行为,只对你指定的方法进行模拟。这在测试复杂对象时特别有用。

public class UserService {
   
    public User getUser(String id) {
   
        User user = fetchFromDatabase(id);
        if (user != null) {
   
            user = enrichUserData(user);
        }
        return user;
    }

    protected User fetchFromDatabase(String id) {
   
        // 从数据库获取用户的逻辑
        return new User(id, "defaultName");
    }

    private User enrichUserData(User user) {
   
        // 丰富用户数据的逻辑
        user.setLastLoginTime(new Date());
        return user;
    }
}

@Test
void testUserServiceWithSpy() {
   
    UserService userService = new UserService();
    UserService spyUserService = spy(userService);

    // 只模拟数据库查询方法,其他方法保持原有逻辑
    User mockUser = new User("123", "张三");
    doReturn(mockUser).when(spyUserService).fetchFromDatabase("123");

    // 调用getUser方法,会执行真实的enrichUserData逻辑
    User result = spyUserService.getUser("123");

    assertEquals("张三", result.getName());
    assertNotNull(result.getLastLoginTime()); // 验证enrichUserData被执行了

    // 验证fetchFromDatabase被调用了
    verify(spyUserService).fetchFromDatabase("123");
}

精确验证也很重要。不仅要验证方法被调用了,还要验证调用的次数、顺序等等。

@Test
void testMethodCallTimes() {
   
    List<String> mockList = mock(List.class);

    // 调用几次add方法
    mockList.add("第一次");
    mockList.add("第二次");
    mockList.add("第三次");

    // 验证add方法被调用了3次
    verify(mockList, times(3)).add(anyString());

    // 验证特定参数的调用次数
    verify(mockList, times(1)).add("第一次");

    // 验证clear方法从未被调用
    verify(mockList, never()).clear();
}

3.3 参数匹配与捕获

参数匹配器是Mockito的一个强大功能。通过any()eq()argThat()等方法,你可以灵活地匹配各种参数。

这样可以让你的测试覆盖更广泛的输入情况。

@Test
void testParameterMatching() {
   
    UserService userService = mock(UserService.class);

    // 匹配任何字符串参数
    when(userService.getUser(anyString())).thenReturn(new User("default"));

    // 匹配特定值
    when(userService.getUser(eq("admin"))).thenReturn(new User("管理员"));

    // 自定义匹配条件
    when(userService.getUser(argThat(id -> id.length() > 5)))
        .thenReturn(new User("长ID用户"));

    // 测试各种情况
    assertEquals("管理员", userService.getUser("admin").getName());
    assertEquals("长ID用户", userService.getUser("123456").getName());
    assertEquals("default", userService.getUser("123").getName());
}

参数捕获在验证复杂对象时特别有用。你可以捕获方法调用时的参数,然后详细验证参数的内容。

public class NotificationService {
   
    public void sendNotification(Notification notification) {
   
        // 发送通知的逻辑
        System.out.println("发送通知: " + notification.getMessage());
    }
}

public class OrderService {
   
    private NotificationService notificationService;

    public OrderService(NotificationService notificationService) {
   
        this.notificationService = notificationService;
    }

    public void processOrder(Order order) {
   
        // 处理订单逻辑
        if (order.getAmount() > 1000) {
   
            Notification notification = new Notification(
                "高额订单", 
                "订单金额: " + order.getAmount() + "元"
            );
            notificationService.sendNotification(notification);
        }
    }
}

@Test
void testOrderProcessingWithCaptor() {
   
    NotificationService notificationService = mock(NotificationService.class);
    OrderService orderService = new OrderService(notificationService);

    // 创建一个高额订单
    Order order = new Order("ORD001", 1500.0);
    orderService.processOrder(order);

    // 捕获发送的通知
    ArgumentCaptor<Notification> captor = ArgumentCaptor.forClass(Notification.class);
    verify(notificationService).sendNotification(captor.capture());

    // 验证通知内容
    Notification capturedNotification = captor.getValue();
    assertEquals("高额订单", capturedNotification.getType());
    assertTrue(capturedNotification.getMessage().contains("1500.0"));
}

4. 实际业务场景案例

4.1 用户登录功能测试

用户登录是一个典型的复杂业务场景。涉及用户查询、密码验证、账号状态检查等多个步骤。

通过Mockito,我们可以轻松测试各种情况:正常登录、密码错误、账号被锁定、用户不存在等等。

public class UserService {
   
    private UserRepository userRepository;
    private PasswordEncoder passwordEncoder;

    public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) {
   
        this.userRepository = userRepository;
        this.passwordEncoder = passwordEncoder;
    }

    public LoginResult login(String username, String password) {
   
        // 查找用户
        Optional<User> userOpt = userRepository.findByUsername(username);
        if (!userOpt.isPresent()) {
   
            return new LoginResult(false, "用户不存在");
        }

        User user = userOpt.get();

        // 检查账号状态
        if (!user.isActive()) {
   
            return new LoginResult(false, "账号已被锁定");
        }

        // 验证密码
        if (!passwordEncoder.matches(password, user.getPassword())) {
   
            return new LoginResult(false, "密码错误");
        }

        return new LoginResult(true, "登录成功");
    }
}

@ExtendWith(MockitoExtension.class)
class UserServiceTest {
   

    @Mock
    private UserRepository userRepository;

    @Mock
    private PasswordEncoder passwordEncoder;

    @InjectMocks
    private UserService userService;

    @Test
    void testLoginSuccess() {
   
        // 模拟用户存在且状态正常
        User user = new User("testUser", "hashedPassword", true);
        when(userRepository.findByUsername("testUser")).thenReturn(Optional.of(user));

        // 模拟密码验证成功
        when(passwordEncoder.matches("password", "hashedPassword")).thenReturn(true);

        // 执行登录
        LoginResult result = userService.login("testUser", "password");

        // 验证结果
        assertTrue(result.isSuccess());
        assertEquals("登录成功", result.getMessage());

        // 验证方法调用
        verify(userRepository).findByUsername("testUser");
        verify(passwordEncoder).matches("password", "hashedPassword");
    }

    @Test
    void testLoginUserNotFound() {
   
        // 模拟用户不存在
        when(userRepository.findByUsername("nonexistent")).thenReturn(Optional.empty());

        LoginResult result = userService.login("nonexistent", "password");

        assertFalse(result.isSuccess());
        assertEquals("用户不存在", result.getMessage());

        // 确保没有调用密码验证
        verifyNoInteractions(passwordEncoder);
    }

    @Test
    void testLoginAccountLocked() {
   
        // 模拟用户存在但账号被锁定
        User lockedUser = new User("lockedUser", "hashedPassword", false);
        when(userRepository.findByUsername("lockedUser")).thenReturn(Optional.of(lockedUser));

        LoginResult result = userService.login("lockedUser", "password");

        assertFalse(result.isSuccess());
        assertEquals("账号已被锁定", result.getMessage());

        // 确保没有调用密码验证
        verifyNoInteractions(passwordEncoder);
    }

    @Test
    void testLoginWrongPassword() {
   
        User user = new User("testUser", "hashedPassword", true);
        when(userRepository.findByUsername("testUser")).thenReturn(Optional.of(user));

        // 模拟密码验证失败
        when(passwordEncoder.matches("wrongPassword", "hashedPassword")).thenReturn(false);

        LoginResult result = userService.login("testUser", "wrongPassword");

        assertFalse(result.isSuccess());
        assertEquals("密码错误", result.getMessage());
    }
}

4.2 订单处理流程测试

订单处理是另一个复杂的业务场景。通常涉及库存检查、价格计算、支付处理、库存扣减等多个步骤。

每个步骤都可能出现异常情况,通过Mockito可以逐一测试这些场景。

public class OrderService {
   
    private InventoryService inventoryService;
    private PaymentService paymentService;
    private NotificationService notificationService;

    public OrderService(InventoryService inventoryService, 
                       PaymentService paymentService,
                       NotificationService notificationService) {
   
        this.inventoryService = inventoryService;
        this.paymentService = paymentService;
        this.notificationService = notificationService;
    }

    public OrderResult processOrder(OrderRequest request) {
   
        try {
   
            // 检查库存
            if (!inventoryService.checkStock(request.getProductId(), request.getQuantity())) {
   
                return new OrderResult(false, "库存不足");
            }

            // 计算总价
            double totalAmount = request.getPrice() * request.getQuantity();

            // 处理支付
            PaymentResult paymentResult = paymentService.processPayment(
                new PaymentRequest(request.getUserId(), totalAmount)
            );

            if (!paymentResult.isSuccess()) {
   
                return new OrderResult(false, "支付失败: " + paymentResult.getErrorMessage());
            }

            // 扣减库存
            inventoryService.reduceStock(request.getProductId(), request.getQuantity());

            // 发送通知
            notificationService.sendOrderConfirmation(request.getUserId(), request.getProductId());

            return new OrderResult(true, "订单处理成功");

        } catch (Exception e) {
   
            return new OrderResult(false, "系统异常: " + e.getMessage());
        }
    }
}

@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
   

    @Mock
    private InventoryService inventoryService;

    @Mock
    private PaymentService paymentService;

    @Mock
    private NotificationService notificationService;

    @InjectMocks
    private OrderService orderService;

    @Test
    void testProcessOrderSuccess() {
   
        OrderRequest request = new OrderRequest("USER001", "PROD001", 2, 100.0);

        // 模拟库存充足
        when(inventoryService.checkStock("PROD001", 2)).thenReturn(true);

        // 模拟支付成功
        PaymentResult paymentResult = new PaymentResult(true, "TXN123", null);
        when(paymentService.processPayment(any(PaymentRequest.class))).thenReturn(paymentResult);

        // 执行订单处理
        OrderResult result = orderService.processOrder(request);

        // 验证结果
        assertTrue(result.isSuccess());
        assertEquals("订单处理成功", result.getMessage());

        // 验证各个服务都被正确调用
        verify(inventoryService).checkStock("PROD001", 2);
        verify(inventoryService).reduceStock("PROD001", 2);
        verify(paymentService).processPayment(argThat(req -> 
            req.getUserId().equals("USER001") && req.getAmount() == 200.0
        ));
        verify(notificationService).sendOrderConfirmation("USER001", "PROD001");
    }

    @Test
    void testProcessOrderInsufficientStock() {
   
        OrderRequest request = new OrderRequest("USER001", "PROD001", 5, 100.0);

        // 模拟库存不足
        when(inventoryService.checkStock("PROD001", 5)).thenReturn(false);

        OrderResult result = orderService.processOrder(request);

        assertFalse(result.isSuccess());
        assertEquals("库存不足", result.getMessage());

        // 确保没有调用后续服务
        verifyNoInteractions(paymentService);
        verifyNoInteractions(notificationService);
        verify(inventoryService, never()).reduceStock(anyString(), anyInt());
    }

    @Test
    void testProcessOrderPaymentFailed() {
   
        OrderRequest request = new OrderRequest("USER001", "PROD001", 1, 100.0);

        when(inventoryService.checkStock("PROD001", 1)).thenReturn(true);

        // 模拟支付失败
        PaymentResult paymentResult = new PaymentResult(false, null, "余额不足");
        when(paymentService.processPayment(any(PaymentRequest.class))).thenReturn(paymentResult);

        OrderResult result = orderService.processOrder(request);

        assertFalse(result.isSuccess());
        assertTrue(result.getMessage().contains("支付失败"));

        // 确保没有扣减库存和发送通知
        verify(inventoryService, never()).reduceStock(anyString(), anyInt());
        verifyNoInteractions(notificationService);
    }

    @Test
    void testProcessOrderSystemException() {
   
        OrderRequest request = new OrderRequest("USER001", "PROD001", 1, 100.0);

        // 模拟系统异常
        when(inventoryService.checkStock(anyString(), anyInt()))
            .thenThrow(new RuntimeException("数据库连接失败"));

        OrderResult result = orderService.processOrder(request);

        assertFalse(result.isSuccess());
        assertTrue(result.getMessage().contains("系统异常"));
        assertTrue(result.getMessage().contains("数据库连接失败"));
    }
}

5. 总结

不要过度Mock

虽然Mock很好用,但不要什么都Mock。对于简单的值对象、数据传输对象,直接创建真实对象往往更简单。

Mock应该用在那些难以构造、执行缓慢或者有副作用的依赖上。比如数据库访问、网络请求、文件操作等等。

保持测试的可读性

测试代码也是代码,同样需要保持可读性。给测试方法起个好名字,让人一看就知道在测试什么。

测试的结构要清晰:准备数据、执行操作、验证结果。每个部分都要简洁明了。

测试要有意义

不要为了提高覆盖率而写无意义的测试。每个测试都应该验证一个明确的业务逻辑或者边界条件。

好的测试不仅能发现bug,还能作为代码的活文档,帮助其他开发者理解业务逻辑。

及时维护测试代码

当业务逻辑发生变化时,相应的测试也需要更新。不要让测试代码成为技术债务。

定期review测试代码,删除那些过时的、重复的测试,保持测试套件的健康。

Mockito是一个强大的工具,合理使用它可以让你的测试覆盖率大幅提升。但记住,覆盖率只是手段,不是目的。真正的目标是写出高质量、可维护的代码。

目录
相关文章
|
28天前
|
人工智能 JavaScript API
从 OpenClaw 到 Hermes Agent:安装、迁移、配置、实战演示
本文详解从OpenClaw迁移到Hermes Agent的全过程:Hermes是Nous Research推出的自进化AI Agent,具备记忆闭环、自主生成技能、跨会话学习等独特能力;迁移支持一键导入配置、记忆与技能,兼容Telegram等平台,安装简便,体验更透明高效。(239字)
271 2
|
28天前
|
监控 Java 测试技术
Java 软件测试(二):Mockito与JUnit 5应用
Java 软件测试(二):Mockito与JUnit 5应用
83 0
|
26天前
|
Web App开发 iOS开发
苹果自带浏览器展示不了钉钉二维码
使用window.DTFrameLogin函数,在safari不展示二维码
251 122
|
28天前
|
SQL JSON 关系型数据库
企业级多模态分析计算引擎选型:阿里云 AnalyticDB MySQL 统一分析平台方案
阿里云AnalyticDB MySQL版是PB级云原生实时数据仓库,首创多模态统一分析引擎,单SQL原生支持SQL分析、向量检索、全文搜索与JSON分析,替代3–5套独立系统,综合成本降50%+,运维复杂度降80%,适用于AI+数据融合、多源异构统一查询等企业级场景。
209 17
企业级多模态分析计算引擎选型:阿里云 AnalyticDB MySQL 统一分析平台方案
|
24天前
|
人工智能 安全 前端开发
ECC 讲透:Claude Code 的全能增强包,不只是 Agents 和 Skills
Everything Claude Code(ECC)是2026年爆火的AI编程增强框架,非简单提示词合集,而是集Agents、Skills、Rules、Hooks、MCP与AgentShield安全扫描于一体的“AI编程操作系统”,深度优化Claude Code等Agent Harness,已获近19万Star。(239字)
235 2
ECC 讲透:Claude Code 的全能增强包,不只是 Agents 和 Skills
|
28天前
|
前端开发
记忆面包:QoderWork CN 把厚书读薄
本案例基于阿里QoderWork CN桌面应用,通过安装“UI设计”技能,将学习笔记一键转化为含填空、选择、判断等题型的交互式复习卡片,支持HTML导出,大幅提升备考效率。
1583 9
|
3月前
|
机器学习/深度学习 自然语言处理 算法
大模型应用:从语义理解到最优匹配:大模型赋能的二分图匈牙利算法全解析.93
本文详解“大模型+匈牙利算法(KM)”融合的智能匹配技术:大模型负责语义理解与对齐,将非结构化文本(如岗位描述、简历)转化为0–100分量化权重;KM算法在此基础上求解带权二分图的全局最优匹配。该方案突破人工规则局限,实现精准、自适应、跨场景的智能配对,广泛适用于人岗匹配、题库组卷、客服问答等核心业务。
331 10
|
26天前
|
SQL 分布式计算 大数据
权限全靠管理员拍脑袋?聊聊数据平台里的ABAC和RBAC到底该怎么落地
权限全靠管理员拍脑袋?聊聊数据平台里的ABAC和RBAC到底该怎么落地
203 2
|
26天前
|
传感器 缓存 安全
原生 APP 的核心开发优势
原生APP(iOS/Swift、Android/Kotlin)直编译运行,性能极致、帧率丝滑、延迟极低;深度调用相机、蓝牙、传感器等硬件;零延迟适配新系统;严守平台设计规范,交互自然;安全加固强、崩溃率低;离线能力卓越。金融、游戏、IoT及体验至上的标杆应用首选。(239字)
|
26天前
|
数据可视化 物联网 中间件
RFID实现库存实时可视化智能管理模式
RFID库存智能管理融合电子标签、读写器与WMS/ERP系统,实现非接触、批量、实时的自动采集、追踪与可视化。提升效率5–20倍,准确率≥99.9%,盘点缩至几十分钟,降本70%,助力企业数字化转型。(239字)