JUnit 5 单元测试指南


JUnit 5是Java生态中最流行的单元测试框架,相比JUnit 4有重大改进。以下是JUnit 5的全面使用指南。

JUnit 5 核心组件

  1. JUnit Platform – 测试运行的基础框架
  2. JUnit Jupiter – 新的编程模型和扩展模型
  3. 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 {
    // 自动继承接口中的测试方法
}

最佳实践

  1. 测试命名:使用@DisplayName描述测试意图
  2. 单一职责:每个测试方法只测试一个行为
  3. AAA模式:安排(Arrange)-行动(Act)-断言(Assert)
  4. 避免依赖:测试之间不应有执行顺序依赖
  5. 及时清理:使用@AfterEach清理测试环境
  6. 测试覆盖率:结合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单元测试的首选框架,合理使用可以显著提高代码质量和开发效率。

,

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注