单元测试高级技巧与实践

1. 分支覆盖实现方法

分支覆盖策略

public class OrderService {
    public String processOrder(Order order, User user) {
        if (order == null) { // 分支1
            throw new IllegalArgumentException("订单不能为空");
        }
        
        if (user == null) { // 分支2
            throw new IllegalArgumentException("用户不能为空");
        }
        
        if (!user.isActive()) { // 分支3
            return "用户未激活";
        }
        
        if (order.getAmount() > 1000) { // 分支4
            return processLargeOrder(order, user);
        } else {
            return processNormalOrder(order, user); // 分支5
        }
    }
    
    private String processLargeOrder(Order order, User user) {
        return "大额订单处理完成";
    }
    
    private String processNormalOrder(Order order, User user) {
        return "普通订单处理完成";
    }
}

实现分支覆盖的测试

class OrderServiceTest {
    private OrderService orderService;
    private User activeUser;
    private User inactiveUser;
    private Order normalOrder;
    private Order largeOrder;

    @BeforeEach
    void setUp() {
        orderService = new OrderService();
        activeUser = new User("user1", true);
        inactiveUser = new User("user2", false);
        normalOrder = new Order(500); // 金额小于1000
        largeOrder = new Order(1500); // 金额大于1000
    }

    @Test
    @DisplayName("覆盖分支1: 订单为空时应抛出异常")
    void processOrder_WhenOrderIsNull_ShouldThrowException() {
        // 覆盖 if (order == null) 分支
        assertThrows(IllegalArgumentException.class, 
            () -> orderService.processOrder(null, activeUser));
    }

    @Test
    @DisplayName("覆盖分支2: 用户为空时应抛出异常")
    void processOrder_WhenUserIsNull_ShouldThrowException() {
        // 覆盖 if (user == null) 分支
        assertThrows(IllegalArgumentException.class, 
            () -> orderService.processOrder(normalOrder, null));
    }

    @Test
    @DisplayName("覆盖分支3: 用户未激活时应返回提示")
    void processOrder_WhenUserInactive_ShouldReturnMessage() {
        // 覆盖 if (!user.isActive()) 分支
        String result = orderService.processOrder(normalOrder, inactiveUser);
        assertEquals("用户未激活", result);
    }

    @Test
    @DisplayName("覆盖分支4: 大额订单应特殊处理")
    void processOrder_WhenLargeOrder_ShouldProcessLargeOrder() {
        // 覆盖 if (order.getAmount() > 1000) 分支
        String result = orderService.processOrder(largeOrder, activeUser);
        assertEquals("大额订单处理完成", result);
    }

    @Test
    @DisplayName("覆盖分支5: 普通订单应正常处理")
    void processOrder_WhenNormalOrder_ShouldProcessNormalOrder() {
        // 覆盖 else 分支
        String result = orderService.processOrder(normalOrder, activeUser);
        assertEquals("普通订单处理完成", result);
    }
}

2. 静态方法调用处理

方法1:重构代码避免静态调用

// 重构前 - 难以测试
public class PaymentService {
    public boolean processPayment(double amount) {
        // 直接调用静态方法,难以Mock
        if (PaymentValidator.isValidAmount(amount)) {
            return PaymentProcessor.process(amount);
        }
        return false;
    }
}

// 重构后 - 易于测试
public class PaymentService {
    private final PaymentValidator paymentValidator;
    private final PaymentProcessor paymentProcessor;
    
    public PaymentService(PaymentValidator paymentValidator, 
                         PaymentProcessor paymentProcessor) {
        this.paymentValidator = paymentValidator;
        this.paymentProcessor = paymentProcessor;
    }
    
    public boolean processPayment(double amount) {
        if (paymentValidator.isValidAmount(amount)) {
            return paymentProcessor.process(amount);
        }
        return false;
    }
}

// 接口定义
public interface PaymentValidator {
    boolean isValidAmount(double amount);
}

public interface PaymentProcessor {
    boolean process(double amount);
}

// 具体实现
@Component
public class DefaultPaymentValidator implements PaymentValidator {
    @Override
    public boolean isValidAmount(double amount) {
        return amount > 0 && amount <= 10000;
    }
}

方法2:使用Mockito模拟静态方法(需要额外配置)

// 添加依赖
// testImplementation 'org.mockito:mockito-inline:4.8.0'

class StaticMethodTest {
    
    @Test
    @DisplayName("使用Mockito模拟静态方法")
    void testStaticMethodWithMockito() {
        try (MockedStatic<PaymentUtils> mockedStatic = mockStatic(PaymentUtils.class)) {
            // 设置静态方法的行为
            mockedStatic.when(() -> PaymentUtils.validate(anyDouble()))
                       .thenReturn(true);
            
            PaymentService service = new PaymentService();
            boolean result = service.processPayment(100.0);
            
            assertTrue(result);
            
            // 验证静态方法被调用
            mockedStatic.verify(() -> PaymentUtils.validate(100.0));
        }
    }
}

3. 单元测试常用方法和技巧

3.1 断言技巧

class AssertionTechniquesTest {
    
    @Test
    @DisplayName("多种断言方法示例")
    void assertionExamples() {
        List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
        String result = "Hello World";
        User user = new User("test", 25);
        
        // JUnit断言
        assertEquals("Hello World", result);
        assertNotEquals("Wrong", result);
        assertTrue(result.startsWith("Hello"));
        assertFalse(result.endsWith("Goodbye"));
        assertNull(null);
        assertNotNull(result);
        
        // AssertJ流式断言 - 更强大
        assertThat(result)
            .isNotNull()
            .startsWith("Hello")
            .endsWith("World")
            .hasSize(11)
            .contains(" ")
            .doesNotContain("Error");
            
        assertThat(names)
            .hasSize(3)
            .contains("Alice", "Bob")
            .doesNotContain("David")
            .allMatch(name -> name.length() >= 3)
            .anyMatch(name -> name.startsWith("A"));
            
        assertThat(user)
            .extracting(User::getName, User::getAge)
            .containsExactly("test", 25);
            
        // 异常断言
        assertThatThrownBy(() -> { throw new IllegalArgumentException("错误消息"); })
            .isInstanceOf(IllegalArgumentException.class)
            .hasMessage("错误消息");
    }
}

3.2 Mockito高级用法

class MockitoAdvancedTest {
    
    @Mock
    private UserRepository userRepository;
    
    @Mock
    private EmailService emailService;
    
    @InjectMocks
    private UserService userService;
    
    @Test
    @DisplayName("参数匹配器使用")
    void argumentMatcherExamples() {
        // any() - 匹配任何参数
        given(userRepository.findByEmail(anyString())).willReturn(Optional.empty());
        
        // eq() - 精确匹配
        given(userRepository.findById(eq(1L))).willReturn(Optional.of(new User()));
        
        // 自定义匹配器
        given(userRepository.save(argThat(user -> user.getName().length() > 3)))
            .willReturn(new User());
            
        // 验证调用次数
        verify(userRepository, times(1)).findByEmail(anyString());
        verify(userRepository, never()).delete(any());
    }
    
    @Test
    @DisplayName("Spy使用 - 部分模拟真实对象")
    void spyExample() {
        List<String> realList = new ArrayList<>();
        List<String> spyList = spy(realList);
        
        // 模拟特定方法
        doReturn("mocked").when(spyList).get(0);
        
        // 其他方法使用真实实现
        spyList.add("real");
        
        assertEquals("mocked", spyList.get(0));
        assertEquals(1, spyList.size());
    }
    
    @Test
    @DisplayName("验证调用顺序")
    void verificationInOrder() {
        // 准备数据
        User user = new User("test", "test@example.com");
        
        // 执行操作
        userService.registerUser(user);
        
        // 验证调用顺序
        InOrder inOrder = inOrder(userRepository, emailService);
        inOrder.verify(userRepository).save(user);
        inOrder.verify(emailService).sendWelcomeEmail(user.getEmail());
    }
    
    @Test
    @DisplayName("模拟void方法")
    void mockVoidMethod() {
        // 模拟void方法抛出异常
        doThrow(new RuntimeException("发送失败"))
            .when(emailService).sendWelcomeEmail(anyString());
            
        // 模拟void方法不执行任何操作
        doNothing().when(emailService).sendNotification(anyString());
    }
}

3.3 测试数据构建

class TestDataBuilder {
    
    /**
     * 测试数据构建器模式
     */
    static class UserBuilder {
        private Long id = 1L;
        private String username = "defaultUser";
        private String email = "default@example.com";
        private boolean active = true;
        private int age = 25;
        
        private UserBuilder() {}
        
        public static UserBuilder aUser() {
            return new UserBuilder();
        }
        
        public UserBuilder withId(Long id) {
            this.id = id;
            return this;
        }
        
        public UserBuilder withUsername(String username) {
            this.username = username;
            return this;
        }
        
        public UserBuilder withEmail(String email) {
            this.email = email;
            return this;
        }
        
        public UserBuilder inactive() {
            this.active = false;
            return this;
        }
        
        public UserBuilder withAge(int age) {
            this.age = age;
            return this;
        }
        
        public User build() {
            User user = new User();
            user.setId(id);
            user.setUsername(username);
            user.setEmail(email);
            user.setActive(active);
            user.setAge(age);
            return user;
        }
    }
    
    @Test
    @DisplayName("使用构建器创建测试数据")
    void testWithBuilder() {
        // 创建默认用户
        User defaultUser = UserBuilder.aUser().build();
        
        // 创建特定用户
        User adminUser = UserBuilder.aUser()
            .withUsername("admin")
            .withEmail("admin@example.com")
            .withAge(30)
            .build();
            
        User inactiveUser = UserBuilder.aUser()
            .withUsername("inactive")
            .inactive()
            .build();
            
        assertThat(adminUser.getUsername()).isEqualTo("admin");
        assertThat(inactiveUser.isActive()).isFalse();
    }
}

4. 综合性单元测试示例

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.*;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Stream;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.*;

/**
 * 电商订单服务综合性单元测试示例
 * 涵盖各种测试场景和技巧
 */
@ExtendWith(MockitoExtension.class)
class OrderServiceComprehensiveTest {

    @Mock
    private OrderRepository orderRepository;

    @Mock
    private InventoryService inventoryService;

    @Mock
    private PaymentService paymentService;

    @Mock
    private NotificationService notificationService;

    @InjectMocks
    private OrderService orderService;

    @Captor
    private ArgumentCaptor<Order> orderCaptor;

    @Captor
    private ArgumentCaptor<String> messageCaptor;

    private User testUser;
    private Product product1;
    private Product product2;

    @BeforeEach
    void setUp() {
        // 初始化测试数据
        testUser = User.builder()
                .id(1L)
                .username("testuser")
                .email("test@example.com")
                .balance(new BigDecimal("1000.00"))
                .build();

        product1 = Product.builder()
                .id(101L)
                .name("笔记本电脑")
                .price(new BigDecimal("5999.00"))
                .stock(10)
                .build();

        product2 = Product.builder()
                .id(102L)
                .name("无线鼠标")
                .price(new BigDecimal("199.00"))
                .stock(50)
                .build();
    }

    /**
     * 正常创建订单测试
     */
    @Test
    @DisplayName("创建订单 - 库存充足且支付成功时应创建订单")
    void createOrder_WithSufficientInventoryAndPaymentSuccess_ShouldCreateOrder() {
        // Arrange - 准备测试数据和行为
        OrderCreateRequest request = new OrderCreateRequest(
                testUser.getId(),
                Arrays.asList(
                        new OrderItem(product1.getId(), 1),
                        new OrderItem(product2.getId(), 2)
                )
        );

        // 模拟库存检查
        given(inventoryService.isAvailable(product1.getId(), 1)).willReturn(true);
        given(inventoryService.isAvailable(product2.getId(), 2)).willReturn(true);

        // 模拟支付服务
        given(paymentService.processPayment(any(BigDecimal.class), eq(testUser.getId())))
                .willReturn(new PaymentResult(true, "支付成功", "TXN12345"));

        // 模拟保存订单
        Order savedOrder = createTestOrder();
        given(orderRepository.save(any(Order.class))).willReturn(savedOrder);

        // Act - 执行被测试方法
        OrderResult result = orderService.createOrder(request);

        // Assert - 验证结果
        assertThat(result).isNotNull();
        assertThat(result.isSuccess()).isTrue();
        assertThat(result.getOrderId()).isEqualTo(1001L);
        assertThat(result.getMessage()).contains("创建成功");

        // 验证交互行为
        verify(inventoryService).reserve(product1.getId(), 1);
        verify(inventoryService).reserve(product2.getId(), 2);
        verify(notificationService).sendOrderConfirmation(eq(testUser.getEmail()), anyString());

        // 使用ArgumentCaptor捕获传递的参数
        verify(orderRepository).save(orderCaptor.capture());
        Order capturedOrder = orderCaptor.getValue();
        assertThat(capturedOrder.getTotalAmount()).isEqualByComparingTo("6397.00");
        assertThat(capturedOrder.getStatus()).isEqualTo(OrderStatus.PAID);
    }

    /**
     * 库存不足测试
     */
    @Test
    @DisplayName("创建订单 - 库存不足时应抛出异常")
    void createOrder_WithInsufficientInventory_ShouldThrowException() {
        // Arrange
        OrderCreateRequest request = new OrderCreateRequest(
                testUser.getId(),
                Arrays.asList(new OrderItem(product1.getId(), 20)) // 超过库存数量
        );

        given(inventoryService.isAvailable(product1.getId(), 20)).willReturn(false);

        // Act & Assert
        assertThatThrownBy(() -> orderService.createOrder(request))
                .isInstanceOf(InsufficientInventoryException.class)
                .hasMessageContaining("库存不足");

        // 验证支付服务没有被调用
        verify(paymentService, never()).processPayment(any(), anyLong());
        verify(orderRepository, never()).save(any());
    }

    /**
     * 支付失败测试
     */
    @Test
    @DisplayName("创建订单 - 支付失败时应取消订单并恢复库存")
    void createOrder_WhenPaymentFails_ShouldCancelOrderAndRestoreInventory() {
        // Arrange
        OrderCreateRequest request = new OrderCreateRequest(
                testUser.getId(),
                Arrays.asList(new OrderItem(product1.getId(), 1))
        );

        given(inventoryService.isAvailable(anyLong(), anyInt())).willReturn(true);
        given(paymentService.processPayment(any(), anyLong()))
                .willReturn(new PaymentResult(false, "余额不足", null));

        // Act
        OrderResult result = orderService.createOrder(request);

        // Assert
        assertThat(result.isSuccess()).isFalse();
        assertThat(result.getMessage()).contains("支付失败");

        // 验证库存被恢复
        verify(inventoryService).release(product1.getId(), 1);
        verify(notificationService).sendPaymentFailure(eq(testUser.getEmail()), anyString());
    }

    /**
     * 参数化测试 - 测试多种边界值
     */
    @ParameterizedTest
    @DisplayName("验证订单金额计算 - 多种商品组合")
    @MethodSource("provideOrderItemsForCalculation")
    void calculateOrderTotal_WithVariousItems_ShouldReturnCorrectTotal(
            List<OrderItem> items, BigDecimal expectedTotal) {
        // Arrange
        given(inventoryService.getProductPrice(anyLong())).willReturn(new BigDecimal("100.00"));

        // Act
        BigDecimal total = orderService.calculateOrderTotal(items);

        // Assert
        assertThat(total).isEqualByComparingTo(expectedTotal);
    }

    private static Stream<Arguments> provideOrderItemsForCalculation() {
        return Stream.of(
                // 单件商品
                Arguments.of(Arrays.asList(new OrderItem(1L, 1)), new BigDecimal("100.00")),
                // 多件相同商品
                Arguments.of(Arrays.asList(new OrderItem(1L, 5)), new BigDecimal("500.00")),
                // 多种商品
                Arguments.of(Arrays.asList(
                        new OrderItem(1L, 2),
                        new OrderItem(2L, 3)
                ), new BigDecimal("500.00")),
                // 零数量(边界情况)
                Arguments.of(Arrays.asList(new OrderItem(1L, 0)), BigDecimal.ZERO)
        );
    }

    /**
     * 超时测试
     */
    @Test
    @DisplayName("批量处理订单 - 应在指定时间内完成")
    void processBatchOrders_ShouldCompleteWithinTimeout() {
        // Arrange
        List<Long> orderIds = Arrays.asList(1L, 2L, 3L, 4L, 5L);
        given(orderRepository.findAllById(orderIds)).willReturn(createTestOrders());

        // Act & Assert - 验证方法在5秒内完成
        assertTimeoutPreemptively(java.time.Duration.ofSeconds(5), () -> {
            BatchProcessResult result = orderService.processBatchOrders(orderIds);
            assertThat(result.getProcessedCount()).isEqualTo(5);
        });
    }

    /**
     * 并发测试示例
     */
    @Test
    @DisplayName("并发更新库存 - 应正确处理竞争条件")
    void concurrentInventoryUpdate_ShouldHandleRaceCondition() throws InterruptedException {
        // Arrange
        int threadCount = 10;
        CountDownLatch startLatch = new CountDownLatch(1);
        CountDownLatch endLatch = new CountDownLatch(threadCount);
        AtomicInteger successCount = new AtomicInteger(0);

        given(inventoryService.isAvailable(product1.getId(), 1)).willReturn(true);
        given(paymentService.processPayment(any(), anyLong()))
                .willReturn(new PaymentResult(true, "成功", "TXN"));

        // Act - 模拟并发请求
        for (int i = 0; i < threadCount; i++) {
            new Thread(() -> {
                try {
                    startLatch.await();
                    OrderCreateRequest request = new OrderCreateRequest(
                            testUser.getId(),
                            Arrays.asList(new OrderItem(product1.getId(), 1))
                    );
                    try {
                        orderService.createOrder(request);
                        successCount.incrementAndGet();
                    } catch (Exception e) {
                        // 预期部分请求会失败
                    }
                    endLatch.countDown();
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }).start();
        }

        startLatch.countDown();
        endLatch.await(5, TimeUnit.SECONDS);

        // Assert - 验证只有部分请求成功(取决于业务逻辑)
        assertThat(successCount.get()).isGreaterThan(0);
        assertThat(successCount.get()).isLessThanOrEqualTo(product1.getStock());
    }

    /**
     * 验证回调调用
     */
    @Test
    @DisplayName("订单状态更新 - 应调用相应的监听器")
    void updateOrderStatus_ShouldInvokeCorrectListeners() {
        // Arrange
        Order order = createTestOrder();
        given(orderRepository.findById(1001L)).willReturn(Optional.of(order));

        // Act
        orderService.updateOrderStatus(1001L, OrderStatus.SHIPPED);

        // Assert
        verify(notificationService).sendShippingNotification(eq(testUser.getEmail()), anyString());
        verify(orderRepository).save(orderCaptor.capture());
        
        Order updatedOrder = orderCaptor.getValue();
        assertThat(updatedOrder.getStatus()).isEqualTo(OrderStatus.SHIPPED);
        assertThat(updatedOrder.getUpdatedAt()).isNotNull();
    }

    /**
     * 测试私有方法(通过公共方法间接测试)
     */
    @Test
    @DisplayName("订单验证逻辑 - 通过公共方法测试私有验证逻辑")
    void validateOrder_ThroughPublicMethod_ShouldCheckAllConstraints() {
        // 通过创建订单来测试内部的validateOrder逻辑
        OrderCreateRequest invalidRequest = new OrderCreateRequest(
                null, // 用户ID为空
                Collections.emptyList() // 空订单项
        );

        assertThatThrownBy(() -> orderService.createOrder(invalidRequest))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessageContaining("用户ID不能为空");
    }

    // 辅助方法
    private Order createTestOrder() {
        return Order.builder()
                .id(1001L)
                .userId(testUser.getId())
                .items(Arrays.asList(
                        new OrderItem(product1.getId(), 1, product1.getPrice()),
                        new OrderItem(product2.getId(), 2, product2.getPrice())
                ))
                .totalAmount(new BigDecimal("6397.00"))
                .status(OrderStatus.PAID)
                .createdAt(LocalDateTime.now())
                .build();
    }

    private List<Order> createTestOrders() {
        List<Order> orders = new ArrayList<>();
        for (int i = 1; i <= 5; i++) {
            orders.add(Order.builder()
                    .id((long) i)
                    .userId(testUser.getId())
                    .totalAmount(new BigDecimal("100.00"))
                    .status(OrderStatus.PENDING)
                    .build());
        }
        return orders;
    }
}

// 支持类定义
class Order {
    private Long id;
    private Long userId;
    private List<OrderItem> items;
    private BigDecimal totalAmount;
    private OrderStatus status;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
    
    // builder, getters, setters
}

class OrderItem {
    private Long productId;
    private Integer quantity;
    private BigDecimal price;
    
    // constructor, getters
}

enum OrderStatus {
    PENDING, PAID, SHIPPED, DELIVERED, CANCELLED
}

class OrderCreateRequest {
    private Long userId;
    private List<OrderItem> items;
    
    // constructor, getters
}

class OrderResult {
    private boolean success;
    private Long orderId;
    private String message;
    
    // constructor, getters
}

class PaymentResult {
    private boolean success;
    private String message;
    private String transactionId;
    
    // constructor, getters
}

class BatchProcessResult {
    private int processedCount;
    private int successCount;
    private int failureCount;
    
    // constructor, getters
}

// 自定义异常
class InsufficientInventoryException extends RuntimeException {
    public InsufficientInventoryException(String message) {
        super(message);
    }
}

关键技巧总结

  1. 分支覆盖:为每个if-else分支编写测试用例
  2. 静态方法处理:优先重构,必要时使用Mockito-inline
  3. 参数化测试:使用@ParameterizedTest测试多组数据
  4. ArgumentCaptor:捕获方法参数进行详细验证
  5. 并发测试:验证多线程环境下的行为
  6. 超时测试:确保性能要求
  7. BDD风格:given-when-then使测试更易读
  8. 测试数据构建器:创建灵活可复用的测试数据

这个综合性示例展示了在实际企业级开发中可能遇到的各种测试场景和相应的解决方案。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

龙茶清欢

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值