Java单元测试最佳实践案例

wen java案例 3

Java单元测试最佳实践案例

基础单元测试示例

使用JUnit 5编写最基本的单元测试

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Assertions;
import java.time.LocalDate;
public class UserServiceTest {
    private UserService userService;
    private UserRepository userRepository;
    @BeforeEach
    void setUp() {
        // 创建真实依赖(或使用Mockito创建mock)
        userRepository = new InMemoryUserRepository();
        userService = new UserService(userRepository);
    }
    @Test
    @DisplayName("创建用户成功场景")
    void shouldCreateUserSuccessfully() {
        // 准备数据
        User user = new User("张三", 25, "zhangsan@example.com");
        // 执行操作
        User result = userService.createUser(user);
        // 验证结果
        Assertions.assertNotNull(result);
        Assertions.assertEquals("张三", result.getName());
        Assertions.assertNotNull(result.getId());
        Assertions.assertEquals(UserStatus.ACTIVE, result.getStatus());
    }
    @Test
    @DisplayName("创建用户时邮箱格式校验")
    void shouldRejectInvalidEmail() {
        User user = new User("李四", 30, "invalid-email");
        IllegalArgumentException exception = Assertions.assertThrows(
            IllegalArgumentException.class,
            () -> userService.createUser(user)
        );
        Assertions.assertEquals("Invalid email format", exception.getMessage());
    }
    @Nested
    @DisplayName("用户查询功能测试")
    class UserQuerying {
        @Test
        @DisplayName("按ID查询存在的用户")
        void shouldFindUserById() {
            User user = new User("王五", 28, "wangwu@example.com");
            userService.createUser(user);
            User found = userService.getUserById(user.getId());
            Assertions.assertNotNull(found);
            Assertions.assertEquals(user.getId(), found.getId());
            Assertions.assertEquals("王五", found.getName());
        }
        @Test
        @DisplayName("查询不存在的用户返回null")
        void shouldReturnNullForNonExistentUser() {
            User found = userService.getUserById(999L);
            Assertions.assertNull(found);
        }
    }
}

使用Mockito进行Mock测试

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.mockito.Mockito.*;
import static org.mockito.ArgumentMatchers.*;
@ExtendWith(MockitoExtension.class)
public class OrderServiceTest {
    @Mock
    private OrderRepository orderRepository;
    @Mock
    private PaymentGateway paymentGateway;
    @Mock
    private NotificationService notificationService;
    @InjectMocks
    private OrderService orderService;
    @Test
    @DisplayName("创建订单并通知")
    void shouldCreateOrderAndNotifyUser() {
        // 准备mock行为
        Order order = new Order("product123", 99.99, "user123");
        when(orderRepository.save(any(Order.class)))
            .thenAnswer(invocation -> {
                Order savedOrder = invocation.getArgument(0);
                savedOrder.setId(1L);
                return savedOrder;
            });
        when(paymentGateway.prepare(anyDouble())).thenReturn("payment_token_123");
        // 执行测试
        Order result = orderService.createOrder(order);
        // 验证结果
        org.junit.jupiter.api.Assertions.assertEquals(1L, result.getId());
        org.junit.jupiter.api.Assertions.assertEquals("payment_token_123", result.getPaymentToken());
        // 验证交互
        verify(orderRepository).save(order);
        verify(paymentGateway).prepare(99.99);
        verify(notificationService).sendOrderConfirmation(any(Order.class));
        verifyNoMoreInteractions(notificationService);
    }
    @Test
    @DisplayName("订单支付成功后更新状态")
    void shouldUpdateOrderStatusOnPaymentSuccess() {
        // 准备
        Order existingOrder = new Order("product456", 150.00, "user456");
        existingOrder.setId(10L);
        when(orderRepository.findById(10L)).thenReturn(java.util.Optional.of(existingOrder));
        // 执行
        orderService.processPayment(10L);
        // 验证
        verify(orderRepository).findById(10L);
        verify(orderRepository).save(org.mockito.ArgumentMatchers.argThat(
            order -> order.getStatus() == OrderStatus.PAID
        ));
        verify(paymentGateway).processPayment(existingOrder.getPaymentToken());
    }
    @Test
    @DisplayName("订单不存在时抛出异常")
    void shouldThrowExceptionWhenOrderNotFound() {
        when(orderRepository.findById(999L)).thenReturn(java.util.Optional.empty());
        org.junit.jupiter.api.Assertions.assertThrows(
            OrderNotFoundException.class,
            () -> orderService.processPayment(999L)
        );
        verify(paymentGateway, never()).processPayment(anyString());
    }
}

参数化测试

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.EnumSource;
import java.util.stream.Stream;
public class CalculationServiceParameterizedTest {
    @ParameterizedTest
    @ValueSource(ints = {2, 4, 6, 8, 10})
    @DisplayName("偶数判断")
    void shouldIdentifyEvenNumbers(int input) {
        CalculationService calcService = new CalculationService();
        Assertions.assertTrue(calcService.isEven(input));
    }
    @ParameterizedTest
    @CsvSource({
        "10, 5, 15",
        "20, 30, 50",
        "0, 0, 0",
        "-5, 5, 0"
    })
    @DisplayName("加法运算参数化测试")
    void shouldAddNumbersCorrectly(int a, int b, int expected) {
        CalculationService calcService = new CalculationService();
        Assertions.assertEquals(expected, calcService.add(a, b));
    }
    @ParameterizedTest
    @EnumSource(ErrorCode.class)
    @DisplayName("每个枚举值都有对应的错误消息")
    void shouldHaveErrorMessageForEachCode(ErrorCode errorCode) {
        MessageUtil messageUtil = new MessageUtil();
        Assertions.assertNotNull(messageUtil.getMessage(errorCode));
        Assertions.assertFalse(messageUtil.getMessage(errorCode).isEmpty());
    }
    @ParameterizedTest
    @MethodSource("provideDataForPrimeCheck")
    @DisplayName("素数判断参数化测试")
    void shouldCorrectlyCheckPrimeNumbers(int number, boolean expected) {
        CalculationService calcService = new CalculationService();
        Assertions.assertEquals(expected, calcService.isPrime(number));
    }
    private static Stream<Arguments> provideDataForPrimeCheck() {
        return Stream.of(
            Arguments.of(2, true),
            Arguments.of(3, true),
            Arguments.of(4, false),
            Arguments.of(17, true),
            Arguments.of(20, false),
            Arguments.of(97, true)
        );
    }
}

测试隔离与Mock Bean(Spring Boot)

import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.*;
@SpringBootTest
@ExtendWith(SpringExtension.class)  // 如果使用JUnit 4
public class SpringBootUserControllerTest {
    @Autowired
    private UserController userController;
    @MockBean
    private UserService userService;
    @MockBean
    private CacheManager cacheManager;
    @Test
    @DisplayName("测试用户注册接口")
    void testUserRegistration() {
        // 准备mock数据
        UserDTO userDTO = new UserDTO("test@example.com", "password123", "张三");
        User mockUser = new User(1L, "test@example.com", "张三");
        when(userService.register(any(UserDTO.class))).thenReturn(mockUser);
        // 执行测试
        ResponseEntity<User> response = userController.register(userDTO);
        // 验证
        Assertions.assertEquals(HttpStatus.CREATED, response.getStatusCode());
        Assertions.assertNotNull(response.getBody());
        Assertions.assertEquals(1L, response.getBody().getId());
        verify(userService, times(1)).register(any(UserDTO.class));
    }
    @Test
    @DisplayName("测试缓存失效场景")
    void testUserCacheEviction() {
        Long userId = 100L;
        when(userService.getUserById(userId)).thenReturn(new User(userId, "test@example.com", "李四"));
        User result1 = userController.getUser(userId);
        User result2 = userController.getUser(userId);
        // 验证缓存被调用
        verify(userService, atMost(1)).getUserById(userId); // 如果缓存实现正确,应该只调用一次
        Assertions.assertEquals(result1, result2);
    }
}

单元测试通用最佳实践模式

AAA模式(Arrange-Act-Assert)

@Test
@DisplayName("用户年龄成年判断")
void shouldCheckIfUserIsAdult() {
    // Arrange - 准备
    User user = new User("测试用户", 25, "test@example.com");
    AgeValidator validator = new AgeValidator();
    // Act - 执行
    boolean isAdult = validator.isAdult(user);
    // Assert - 验证
    Assertions.assertTrue(isAdult);
}

使用自定义断言或断言库

import org.assertj.core.api.Assertions;
@Test
@DisplayName("使用AssertJ进行更灵活的断言")
void shouldUseAssertJForFlexibleAssertions() {
    UserService userService = new UserService(new InMemoryUserRepository());
    User user = userService.createUser(new User("用户A", 30, "usera@example.com"));
    // 链式断言
    Assertions.assertThat(user)
        .isNotNull()
        .hasFieldOrPropertyWithValue("name", "用户A")
        .hasFieldOrPropertyWithValue("age", 30)
        .extracting(User::getEmail)
        .asString()
        .contains("@example.com");
    // List断言
    List<User> allUsers = userService.getAllUsers();
    Assertions.assertThat(allUsers)
        .hasSize(1)
        .extracting(User::getName)
        .containsExactly("用户A");
    // 异常断言
    Assertions.assertThatThrownBy(() -> userService.getUserById(null))
        .isInstanceOf(IllegalArgumentException.class)
        .hasMessageContaining("ID cannot be null");
}

测试数据工厂

public class TestDataFactory {
    public static User createDefaultUser() {
        return createUser("测试用户", 25, "test@example.com");
    }
    public static User createUser(String name, int age, String email) {
        User user = new User();
        user.setName(name);
        user.setAge(age);
        user.setEmail(email);
        user.setStatus(UserStatus.NEW);
        return user;
    }
    public static Order createOrderWithDefaultUser() {
        return Order.builder()
            .user(createDefaultUser())
            .totalAmount(99.99)
            .status(OrderStatus.PENDING)
            .items(List.of(
                new OrderItem("product_1", "商品1", 2),
                new OrderItem("product_2", "商品2", 1)
            ))
            .build();
    }
}
// 在测试中使用
@Test
void shouldCreateOrderSuccessfully() {
    Order order = TestDataFactory.createOrderWithDefaultUser();
    OrderStatus status = orderService.placeOrder(order);
    Assertions.assertEquals(OrderStatus.CONFIRMED, status);
    verify(orderRepository).save(order);
}

测试命名规范

public class PaymentServiceTest {
    @Test
    @DisplayName("有效支付方式时处理支付成功")
    void handlePayment_withValidPaymentMethod_shouldSucceed() {
        // 可以使用 given_when_then 或者 When_Then 命名
    }
    @Test
    @DisplayName("无效支付方式时抛出异常")
    void handlePayment_withInvalidPaymentMethod_shouldThrowException() {
        // 测试代码
    }
    // 另一种规范
    @Test
    @DisplayName("should_fail_when_payment_amount_is_negative")
    void should_fail_when_payment_amount_is_negative() {
        // 测试代码
    }
}

使用Testcontainers进行外部依赖测试(可选)

import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
@Testcontainers
@SpringBootTest
public class UserRepositoryIntegrationTest {
    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15")
            .withDatabaseName("testdb")
            .withUsername("test")
            .withPassword("test");
    @DynamicPropertySource
    static void configureDatabase(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
    }
    @Autowired
    private UserRepository userRepository;
    @Test
    @DisplayName("用户保存和查询功能")
    void shouldSaveAndRetrieveUser() {
        User user = TestDataFactory.createDefaultUser();
        userRepository.save(user);
        Optional<User> found = userRepository.findById(user.getId());
        Assertions.assertTrue(found.isPresent());
        Assertions.assertEquals(user.getEmail(), found.get().getEmail());
    }
}

测试覆盖率与常用工具

// 使用覆盖率工具插件(如JaCoCo)后,可生成HTML报告
// 重要:高覆盖率不代表测试质量,应关注关键路径
// 使用LocalDateTime测试时间相关功能
@Test
@DisplayName("测试用户注册时间记录")
void shouldRecordRegistrationTime() {
    LocalDateTime fixedTime = LocalDateTime.of(2025, 1, 15, 10, 30);
    TimeProvider timeProvider = () -> fixedTime; // 注入可以mock的时间提供器
    UserService userService = new UserService(new InMemoryUserRepository(), timeProvider);
    User user = userService.register("test@example.com", "password");
    Assertions.assertEquals(fixedTime, user.getRegistrationTime());
}

并发和异常测试

@RepeatedTest(5)
@DisplayName("并发注册100个用户不应崩溃")
void concurrentUserRegistration() {
    ExecutorService executor = Executors.newFixedThreadPool(10);
    CountDownLatch latch = new CountDownLatch(100);
    for (int i = 0; i < 100; i++) {
        executor.submit(() -> {
            try {
                int userId = ThreadLocalRandom.current().nextInt(0, 1000);
                userService.register("user" + userId + "@example.com", "password");
            } catch (Exception e) {
                // 记录异常
            } finally {
                latch.countDown();
            }
        });
    }
    try {
        latch.await(10, TimeUnit.SECONDS);
        Assertions.assertTrue(userService.getUserCount() >= 100);
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        Assertions.fail("Test interrupted");
    }
}

测试代码质量检查清单

检查项 示例/说明
测试独立性 每个测试互不影响,不共享状态
测试可靠性 不依赖网络、时间、随机数(除非mock)
测试表述性 使用@DisplayName描述测试意图
数据清理 @AfterEach中清理测试数据
边界条件 测试空值、最大值、负数等边界值
异常路径 不仅测试成功场景,还要测试失败和异常
不测试私有方法 通过公有API间接测试私有逻辑

好的单元测试应该是:

Java单元测试最佳实践案例

  1. 快速 - 可单独运行,不依赖外部服务
  2. 可靠 - 结果稳定,不含随机性
  3. 可读 - 他人能快速理解测试意图
  4. 聚焦 - 一个测试只验证一个行为
  5. 覆盖关键 - 关注业务规则和关键分支

实际项目中,建议结合 TDD(测试驱动开发)BDD(行为驱动开发) 方式开展工作,使测试产出最大化价值。

抱歉,评论功能暂时关闭!