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

举报
Yeats_Liao 发表于 2025/11/12 10:51:05 2025/11/12
【摘要】 写单元测试的时候,经常会遇到一个问题:覆盖率总是上不去。明明写了很多测试,但还是有很多代码分支没有被执行到。其实这个问题很常见,特别是在处理复杂业务逻辑的时候。代码覆盖率不仅仅是一个数字,它反映了你的测试到底测了多少代码。 1. 代码覆盖率到底有什么用覆盖率的真正意义代码覆盖率说白了就是告诉你:测试代码执行了多少源代码。包括代码行、分支、路径这些维度。高覆盖率确实能帮你发现一些潜在的bug,...

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

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

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

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。