JUnit 5是Java生态中最流行的单元测试框架,相比JUnit 4有重大改进。以下是JUnit 5的全面使用指南。
JUnit 5 核心组件
- JUnit Platform – 测试运行的基础框架
- JUnit Jupiter – 新的编程模型和扩展模型
- JUnit Vintage – 兼容JUnit 3/4的测试引擎
基础配置
Maven依赖
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.9.3</version>
<scope>test</scope>
</dependency>
Gradle依赖
dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter:5.9.3'
}
test {
useJUnitPlatform()
}
基础注解
注解 | 说明 |
---|---|
@Test | 标记测试方法 |
@BeforeEach | 每个测试方法前执行 |
@AfterEach | 每个测试方法后执行 |
@BeforeAll | 所有测试方法前执行(静态方法) |
@AfterAll | 所有测试方法后执行(静态方法) |
@DisplayName | 为测试类或方法指定显示名称 |
@Disabled | 禁用测试类或方法 |
基本测试示例
import org.junit.jupiter.api.*;
class CalculatorTest {
Calculator calculator;
@BeforeAll
static void initAll() {
System.out.println("--初始化所有测试--");
}
@BeforeEach
void init() {
calculator = new Calculator();
System.out.println("初始化测试实例");
}
@Test
@DisplayName("加法测试")
void testAddition() {
assertEquals(4, calculator.add(2, 2));
}
@Test
@Disabled("暂未实现")
void testSubtraction() {
// 待实现
}
@AfterEach
void tearDown() {
System.out.println("清理测试环境");
}
@AfterAll
static void tearDownAll() {
System.out.println("--所有测试完成--");
}
}
断言方法
JUnit Jupiter提供了Assertions
类中的多种断言方法:
import static org.junit.jupiter.api.Assertions.*;
@Test
void standardAssertions() {
assertEquals(2, calculator.add(1, 1));
assertTrue('a' < 'b', "断言消息可自定义");
assertNull(null, "应为null");
assertNotNull(new Object());
assertThrows(ArithmeticException.class, () -> calculator.divide(1, 0));
assertTimeout(Duration.ofMillis(100), () -> {
// 应在100ms内完成的代码
});
}
参数化测试
使用@ParameterizedTest
和不同的参数源:
@ParameterizedTest
@ValueSource(ints = {1, 3, 5, -3, 15})
void isOdd_ShouldReturnTrueForOddNumbers(int number) {
assertTrue(calculator.isOdd(number));
}
@ParameterizedTest
@CsvSource({
"1, 1, 2",
"2, 3, 5",
"10, 20, 30"
})
void add_ShouldReturnCorrectSum(int a, int b, int expected) {
assertEquals(expected, calculator.add(a, b));
}
@ParameterizedTest
@MethodSource("stringProvider")
void testWithMethodSource(String argument) {
assertNotNull(argument);
}
static Stream<String> stringProvider() {
return Stream.of("apple", "banana");
}
动态测试
运行时生成测试用例:
@TestFactory
Stream<DynamicTest> dynamicTestsFromStream() {
return Stream.of("A", "B", "C")
.map(str -> DynamicTest.dynamicTest("Test " + str,
() -> assertTrue(str.length() == 1)));
}
测试生命周期回调
通过扩展模型实现更复杂的测试行为:
class TimingExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback {
@Override
public void beforeTestExecution(ExtensionContext context) {
getStore(context).put("start", System.currentTimeMillis());
}
@Override
public void afterTestExecution(ExtensionContext context) {
long start = getStore(context).get("start", long.class);
System.out.println("测试耗时: " + (System.currentTimeMillis() - start) + "ms");
}
private Store getStore(ExtensionContext context) {
return context.getStore(Namespace.create(getClass(), context.getRequiredTestMethod()));
}
}
@ExtendWith(TimingExtension.class)
class TimedTests {
@Test
void sleep20ms() throws Exception {
Thread.sleep(20);
}
}
嵌套测试
使用@Nested
组织相关测试:
@DisplayName("栈测试")
class StackTest {
Stack<Object> stack;
@Test
void isInstantiatedWithNew() {
new Stack<>();
}
@Nested
@DisplayName("当新建时")
class WhenNew {
@BeforeEach
void createNewStack() {
stack = new Stack<>();
}
@Test
@DisplayName("为空")
void isEmpty() {
assertTrue(stack.isEmpty());
}
@Nested
@DisplayName("压入元素后")
class AfterPushing {
String element = "element";
@BeforeEach
void pushElement() {
stack.push(element);
}
@Test
@DisplayName("栈不为空")
void isNotEmpty() {
assertFalse(stack.isEmpty());
}
}
}
}
测试接口和默认方法
可以在接口中定义测试模板:
interface TestLifecycleLogger {
@BeforeAll
static void beforeAllTests() {
System.out.println("测试即将开始");
}
@AfterAll
static void afterAllTests() {
System.out.println("测试已完成");
}
@Test
default void testEqualStrings() {
assertEquals("foo", "foo");
}
}
class ImplementsTestLifecycleLogger implements TestLifecycleLogger {
// 自动继承接口中的测试方法
}
最佳实践
- 测试命名:使用
@DisplayName
描述测试意图 - 单一职责:每个测试方法只测试一个行为
- AAA模式:安排(Arrange)-行动(Act)-断言(Assert)
- 避免依赖:测试之间不应有执行顺序依赖
- 及时清理:使用
@AfterEach
清理测试环境 - 测试覆盖率:结合JaCoCo等工具确保足够覆盖率
与其他库集成
Mockito (模拟对象)
@ExtendWith(MockitoExtension.class)
class ServiceTest {
@Mock
private Repository repository;
@InjectMocks
private Service service;
@Test
void testFindById() {
when(repository.findById(1L)).thenReturn(new Entity(1L, "test"));
Entity result = service.findById(1L);
assertEquals("test", result.getName());
verify(repository).findById(1L);
}
}
AssertJ (流式断言)
import static org.assertj.core.api.Assertions.*;
@Test
void assertJExample() {
List<String> list = Arrays.asList("a", "b", "c");
assertThat(list)
.hasSize(3)
.contains("a", "b")
.doesNotContain("d");
}
JUnit 5的灵活性和强大功能使其成为Java单元测试的首选框架,合理使用可以显著提高代码质量和开发效率。