JUnit 单元测试详细使用指南
1. JUnit 概述
JUnit 是 Java 最流行的单元测试框架,用于编写和运行可重复的测试。
1.1 JUnit 版本
- JUnit 4:使用注解,需要手动导入
- JUnit 5:模块化设计,功能更强大(推荐)
2. 环境配置
2.1 Maven 依赖
xml
<!-- JUnit 5 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.9.2</version>
<scope>test</scope>
</dependency>
<!-- JUnit 4 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
2.2 项目结构
src/
├── main/
│ └── java/
│ └── com/example/
│ └── UserService.java
└── test/
└── java/
└── com/example/
└── UserServiceTest.java
3. JUnit 5 核心用法
3.1 基本测试类
java
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class CalculatorTest {
private final Calculator calculator = new Calculator();
@Test
void testAddition() {
// 给定条件
int a = 5;
int b = 3;
// 执行操作
int result = calculator.add(a, b);
// 验证结果
assertEquals(8, result, "5 + 3 应该等于 8");
}
@Test
void testDivision() {
assertEquals(2, calculator.divide(6, 3));
}
@Test
void testDivisionByZero() {
// 测试异常情况
assertThrows(ArithmeticException.class, () -> {
calculator.divide(5, 0);
});
}
}
3.2 生命周期注解
java
import org.junit.jupiter.api.*;
class LifecycleTest {
@BeforeAll
static void setUpBeforeAll() {
System.out.println("在所有测试方法之前执行一次 - 用于初始化静态资源");
}
@AfterAll
static void tearDownAfterAll() {
System.out.println("在所有测试方法之后执行一次 - 用于清理静态资源");
}
@BeforeEach
void setUp() {
System.out.println("在每个测试方法之前执行 - 用于初始化测试数据");
}
@AfterEach
void tearDown() {
System.out.println("在每个测试方法之后执行 - 用于清理资源");
}
@Test
void testOne() {
System.out.println("执行测试一");
assertTrue(true);
}
@Test
void testTwo() {
System.out.println("执行测试二");
assertFalse(false);
}
}
3.3 断言方法
java
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class AssertionsTest {
@Test
void testVariousAssertions() {
String nullString = null;
String actualString = "Hello JUnit";
// 基本断言
assertEquals(4, 2 + 2);
assertNotEquals(3, 1 + 1);
// 布尔断言
assertTrue(5 > 3);
assertFalse(5 < 3);
// 空值断言
assertNull(nullString);
assertNotNull(actualString);
// 数组断言
assertArrayEquals(new int[]{1, 2, 3}, new int[]{1, 2, 3});
// 异常断言
Exception exception = assertThrows(ArithmeticException.class, () -> {
int result = 1 / 0;
});
// 超时断言
assertTimeout(Duration.ofSeconds(1), () -> {
Thread.sleep(500);
});
// 组合断言 - 所有断言都必须通过
assertAll("多个断言",
() -> assertEquals(4, 2 + 2),
() -> assertTrue(5 > 3),
() -> assertNotNull(actualString)
);
}
}
4. 高级特性
4.1 参数化测试
java
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.*;
class ParameterizedTestExample {
@ParameterizedTest
@ValueSource(ints = {1, 3, 5, -3, 15})
void testIsOdd(int number) {
assertTrue(number % 2 != 0);
}
@ParameterizedTest
@CsvSource({
"2, 3, 5",
"0, 0, 0",
"-1, 2, 1",
"10, -5, 5"
})
void testAddition(int a, int b, int expected) {
Calculator calculator = new Calculator();
assertEquals(expected, calculator.add(a, b));
}
@ParameterizedTest
@MethodSource("stringProvider")
void testWithMethodSource(String argument) {
assertNotNull(argument);
}
static Stream<String> stringProvider() {
return Stream.of("apple", "banana", "orange");
}
}
4.2 测试套件
java
import org.junit.platform.suite.api.*;
@Suite
@SelectPackages("com.example.service")
@IncludeClassNamePatterns(".*Test")
@ExcludeTags("slow")
class TestSuite {
// 运行 com.example.service 包下所有以 Test 结尾的测试类,排除标记为 slow 的测试
}
4.3 条件测试
java
import org.junit.jupiter.api.condition.*;
class ConditionalTest {
@Test
@EnabledOnOs(OS.WINDOWS)
void onlyOnWindows() {
// 只在 Windows 系统上运行
}
@Test
@DisabledOnOs(OS.MAC)
void notOnMac() {
// 不在 Mac 系统上运行
}
@Test
@EnabledIfSystemProperty(named = "os.arch", matches = ".*64.*")
void onlyOn64BitArchitecture() {
// 只在 64 位架构上运行
}
@Test
@EnabledIf("customCondition")
void onlyIfCustomCondition() {
// 只在自定义条件满足时运行
}
boolean customCondition() {
return System.getProperty("custom.flag") != null;
}
}
5. 实际开发中的测试示例
5.1 Service 层测试
java
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.junit.jupiter.api.Assertions.*;
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository;
@Mock
private EmailService emailService;
@InjectMocks
private UserService userService;
@Test
void testCreateUser() {
// 准备测试数据
User user = new User("john@example.com", "John Doe");
// 设置 Mock 行为
when(userRepository.save(any(User.class))).thenReturn(user);
doNothing().when(emailService).sendWelcomeEmail(anyString());
// 执行测试
User createdUser = userService.createUser(user);
// 验证结果
assertNotNull(createdUser);
assertEquals("john@example.com", createdUser.getEmail());
// 验证 Mock 交互
verify(userRepository, times(1)).save(user);
verify(emailService, times(1)).sendWelcomeEmail("john@example.com");
}
@Test
void testCreateUserWithDuplicateEmail() {
User user = new User("existing@example.com", "Existing User");
when(userRepository.findByEmail("existing@example.com"))
.thenReturn(Optional.of(user));
// 测试异常情况
assertThrows(DuplicateEmailException.class, () -> {
userService.createUser(user);
});
verify(userRepository, never()).save(any(User.class));
}
}
5.2 MyBatis Mapper 测试
java
import org.mybatis.spring.boot.test.autoconfigure.MybatisTest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
@MybatisTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class UserMapperTest {
@Autowired
private UserMapper userMapper;
@Test
void testFindById() {
// 假设测试数据已通过 @Sql 或数据库迁移工具准备
User user = userMapper.findById(1L);
assertNotNull(user);
assertEquals("john@example.com", user.getEmail());
}
@Test
void testInsertUser() {
User newUser = new User();
newUser.setEmail("test@example.com");
newUser.setName("Test User");
int result = userMapper.insert(newUser);
assertEquals(1, result);
assertNotNull(newUser.getId()); // 测试自增ID
User savedUser = userMapper.findById(newUser.getId());
assertEquals("test@example.com", savedUser.getEmail());
}
}
5.3 Spring Boot 集成测试
java
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@SpringBootTest
@AutoConfigureMockMvc
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private UserRepository userRepository;
@Test
void testGetUser() throws Exception {
// 准备测试数据
User user = new User("test@example.com", "Test User");
userRepository.save(user);
// 执行并验证 HTTP 请求
mockMvc.perform(get("/api/users/{id}", user.getId()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.email").value("test@example.com"))
.andExpect(jsonPath("$.name").value("Test User"));
}
}
6. 测试最佳实践
6.1 测试命名规范
java
class UserServiceTest {
// 方式1:方法名描述行为
@Test
void shouldReturnUserWhenValidIdProvided() {
// 测试逻辑
}
// 方式2:Given-When-Then 模式
@Test
void givenValidUserId_whenFindById_thenReturnUser() {
// given - 准备数据
Long userId = 1L;
User expectedUser = new User(userId, "test@example.com");
// when - 执行操作
User actualUser = userService.findById(userId);
// then - 验证结果
assertEquals(expectedUser, actualUser);
}
// 方式3:简单描述
@Test
void testFindUserById() {
// 测试逻辑
}
}
6.2 测试组织结构
java
class ComplexServiceTest {
@Nested
@DisplayName("创建用户场景")
class CreateUserScenarios {
@Test
@DisplayName("当提供有效用户信息时,应该成功创建用户")
void createUserWithValidData() {
// 测试正常流程
}
@Test
@DisplayName("当提供重复邮箱时,应该抛出重复邮箱异常")
void createUserWithDuplicateEmail() {
// 测试异常流程
}
}
@Nested
@DisplayName("更新用户场景")
class UpdateUserScenarios {
@Test
void updateUserWithValidData() {
// 测试更新逻辑
}
}
}
7. 常见易错点
7.1 测试独立性
java
// 错误:测试之间有依赖
class DependentTest {
private static int counter = 0;
@Test
void testOne() {
counter++;
assertEquals(1, counter); // 依赖于执行顺序
}
@Test
void testTwo() {
counter++;
assertEquals(2, counter); // 可能失败
}
}
// 正确:每个测试独立
class IndependentTest {
@Test
void testOne() {
int result = calculate(1);
assertEquals(2, result);
}
@Test
void testTwo() {
int result = calculate(2);
assertEquals(3, result);
}
private int calculate(int input) {
return input + 1;
}
}
7.2 避免过于复杂的测试
java
// 错误:一个测试验证太多功能
@Test
void testUserRegistration() {
// 验证用户创建
// 验证邮件发送
// 验证日志记录
// 验证数据库状态
// 验证缓存更新
// ... 过于复杂
}
// 正确:拆分关注点
@Test
void testUserCreation() {
// 只关注用户创建
}
@Test
void testEmailSending() {
// 只关注邮件发送
}
@Test
void testLogging() {
// 只关注日志记录
}
7.3 正确的异常测试
java
// 错误:在 try-catch 中手动处理异常
@Test
void testExceptionWrong() {
try {
userService.divide(1, 0);
fail("应该抛出异常");
} catch (ArithmeticException e) {
// 测试通过
}
}
// 正确:使用 assertThrows
@Test
void testExceptionCorrect() {
assertThrows(ArithmeticException.class, () -> {
userService.divide(1, 0);
});
}
8. 常用测试模式
8.1 测试数据构建器
java
class UserTestBuilder {
private Long id;
private String email = "default@example.com";
private String name = "Default User";
public static UserTestBuilder aUser() {
return new UserTestBuilder();
}
public UserTestBuilder withEmail(String email) {
this.email = email;
return this;
}
public UserTestBuilder withName(String name) {
this.name = name;
return this;
}
public User build() {
User user = new User();
user.setId(id);
user.setEmail(email);
user.setName(name);
return user;
}
}
// 使用构建器
@Test
void testWithBuilder() {
User user = UserTestBuilder.aUser()
.withEmail("test@example.com")
.withName("Test User")
.build();
User savedUser = userRepository.save(user);
assertNotNull(savedUser.getId());
}
通过掌握这些 JUnit 使用方法和最佳实践,你可以编写出高质量、可维护的单元测试,显著提升代码质量和开发效率。