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间接测试私有逻辑 |
好的单元测试应该是:

- 快速 - 可单独运行,不依赖外部服务
- 可靠 - 结果稳定,不含随机性
- 可读 - 他人能快速理解测试意图
- 聚焦 - 一个测试只验证一个行为
- 覆盖关键 - 关注业务规则和关键分支
实际项目中,建议结合 TDD(测试驱动开发) 或 BDD(行为驱动开发) 方式开展工作,使测试产出最大化价值。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播