1. 概述

JUnit 和 TestNG 无疑是 Java 生态中最流行的两大单元测试框架。虽然 TestNG 的设计灵感源于 JUnit,但它提供了许多独特功能,并且与 JUnit 不同,TestNG 不仅适用于单元测试,还能支持功能测试和更高级别的测试。

本文将深入对比这两个框架的核心功能和常见使用场景,帮你快速掌握它们的异同点。

2. 测试初始化与清理

编写测试用例时,我们经常需要在测试执行前进行配置或初始化,并在测试完成后执行清理操作。下面分别看看两个框架如何实现这些需求。

JUnit 提供了方法级和类级的初始化/清理注解

  • 方法级:@BeforeEach(每个测试方法前执行)和 @AfterEach(每个测试方法后执行)
  • 类级:@BeforeAll(所有测试方法前执行)和 @AfterAll(所有测试方法后执行)
class SummationServiceTest {

    private static List<Integer> numbers;

    @BeforeAll
    static void initialize() {
        numbers = new ArrayList<>();
    }

    @AfterAll
    static void tearDown() {
        numbers = null;
    }

    @BeforeEach
    void runBeforeEachTest() {
        numbers.add(1);
        numbers.add(2);
        numbers.add(3);
    }

    @AfterEach
    void runAfterEachTest() {
        numbers.clear();
    }

    @Test
    void givenNumbers_sumEquals_thenCorrect() {
        int sum = numbers.stream().reduce(0, Integer::sum);
        assertEquals(6, sum);
    }
}

⚠️ 注意:示例使用的是 JUnit 5。在 JUnit 4 中,对应注解是 @Before/@After(方法级)和 @BeforeClass/@AfterClass(类级)。

TestNG 同样提供方法级和类级的初始化/清理

  • 类级:@BeforeClass@AfterClass
  • 方法级:@BeforeMethod@AfterMethod
@BeforeClass
public void initialize() {
    numbers = new ArrayList<>();
}

@AfterClass
public void tearDown() {
    numbers = null;
}

@BeforeMethod
public void runBeforeEachTest() {
    numbers.add(1);
    numbers.add(2);
    numbers.add(3);
}

@AfterMethod
public void runAfterEachTest() {
    numbers.clear();
}

TestNG 还提供了更高级的套件级和分组级注解

  • @BeforeSuite/@AfterSuite:整个测试套件执行前后
  • @BeforeGroup/@AfterGroup:特定测试组执行前后
@BeforeGroups("positive_tests")
public void runBeforeEachGroup() {
    numbers.add(1);
    numbers.add(2);
    numbers.add(3);
}

@AfterGroups("negative_tests")
public void runAfterEachGroup() {
    numbers.clear(); 
}

此外,TestNG 支持 @BeforeTest/@AfterTest,用于在 XML 配置文件中 <test> 标签包含的测试用例执行前后操作:

<test name="test setup">
    <classes>
        <class name="SummationServiceTest">
            <methods>
                <include name="givenNumbers_sumEquals_thenCorrect" />
            </methods>
        </class>
    </classes>
</test>

✅ 关键差异:JUnit 要求 @BeforeAll/@AfterAll 方法必须是静态的,而 TestNG 没有此限制。

3. 忽略测试

两个框架都支持忽略测试用例,但实现方式不同。

JUnit 5 使用 @Disabled 注解:

@Disabled
@Test
void givenEmptyList_sumEqualsZero_thenCorrect() {
    int sum = numbers.stream().reduce(0, Integer::sum);
    Assert.assertEquals(6, sum);
}

JUnit 4 使用 @Ignore 注解:

@Ignore
@Test
public void givenNumbers_sumEquals_thenCorrect() {
    int sum = numbers.stream().reduce(0, Integer::sum);
    Assert.assertEquals(6, sum);
}

TestNG 通过 @Test 注解的 enabled 参数控制:

@Test(enabled=false)
public void givenNumbers_sumEquals_thenCorrect() {
    int sum = numbers.stream.reduce(0, Integer::sum);
    Assert.assertEquals(6, sum);
}

4. 测试套件执行

两个框架都支持将测试作为集合运行,但实现方式差异明显。

JUnit 5 使用注解组合创建测试套件

  • @Suite:声明测试套件
  • @SelectPackages:按包选择测试
  • @SelectClasses:按类选择测试

按包选择示例:

@Suite
@SelectPackages({ "org.baeldung.java.suite.childpackage1", "org.baeldung.java.suite.childpackage2" })
class SelectPackagesSuiteUnitTest {
}

按类选择示例:

@Suite
@SelectClasses({Class1UnitTest.class, Class2UnitTest.class})
class SelectClassesSuiteUnitTest {
}

⚠️ JUnit 4 需要使用 @RunWith(Suite.class)@Suite.SuiteClasses

@RunWith(Suite.class)
@Suite.SuiteClasses({ RegistrationTest.class, SignInTest.class })
public class SuiteTest {
}

TestNG 通过 XML 文件组织测试套件

<suite name="suite">
    <test name="test suite">
        <classes>
            <class name="com.baeldung.RegistrationTest" />
            <class name="com.baeldung.SignInTest" />
        </classes>
    </test>
</suite>

TestNG 还支持方法级分组

@Test(groups = "regression")
public void givenNegativeNumber_sumLessthanZero_thenCorrect() {
    int sum = numbers.stream().reduce(0, Integer::sum);
    Assert.assertTrue(sum < 0);
}

通过 XML 执行特定分组:

<test name="test groups">
    <groups>
        <run>
            <include name="regression" />
        </run>
    </groups>
    <classes>
        <class name="com.baeldung.SummationServiceTest" />
    </classes>
</test>

5. 异常测试

两个框架都支持通过注解测试异常

先创建一个会抛出异常的类:

public class Calculator {
    public double divide(double a, double b) {
        if (b == 0) {
            throw new DivideByZeroException("Divider cannot be equal to zero!");
        }
        return a/b;
    }
}

JUnit 5 使用 assertThrows API:

@Test
void whenDividerIsZero_thenDivideByZeroExceptionIsThrown() {
    Calculator calculator = new Calculator();
    assertThrows(DivideByZeroException.class, () -> calculator.divide(10, 0));
}

JUnit 4 通过 @Test(expected = ...) 实现:

@Test(expected = DivideByZeroException.class)
public void whenDividerIsZero_thenDivideByZeroExceptionIsThrown() {
    Calculator calculator = new Calculator();
    calculator.divide(10, 0);
}

TestNG 使用 expectedExceptions 参数:

@Test(expectedExceptions = ArithmeticException.class) 
public void givenNumber_whenThrowsException_thenCorrect() { 
    int i = 1 / 0;
}

6. 参数化测试

参数化测试允许用不同输入数据重复执行同一测试逻辑。

JUnit 5 提供多种数据源注解

  1. **@ValueSource**:基本类型数组

    @ParameterizedTest
    @ValueSource(strings = { "Hello", "World" })
    void givenString_TestNullOrNot(String word) {
     assertNotNull(word);
    }
    
  2. **@EnumSource**:枚举常量

    @ParameterizedTest
    @EnumSource(value = PizzaDeliveryStrategy.class, names = {"EXPRESS", "NORMAL"})
    void givenEnum_TestContainsOrNot(PizzaDeliveryStrategy timeUnit) {
     assertTrue(EnumSet.of(PizzaDeliveryStrategy.EXPRESS, PizzaDeliveryStrategy.NORMAL).contains(timeUnit));
    }
    
  3. **@MethodSource**:外部方法生成数据流 ```java static Stream wordDataProvider() { return Stream.of("foo", "bar"); }

@ParameterizedTest @MethodSource("wordDataProvider") void givenMethodSource_TestInputStream(String argument) { assertNotNull(argument); }


4. **`@CsvSource`**:CSV 格式数据
```java
@ParameterizedTest
@CsvSource({ "1, Car", "2, House", "3, Train" })
void givenCSVSource_TestContent(int id, String word) {
    assertNotNull(id);
    assertNotNull(word);
}

✅ 其他数据源:@CsvFileSource(读取 CSV 文件)、@ArgumentSource(自定义数据提供者)

TestNG 通过 @Parameters@DataProvider 实现参数化

  1. XML 参数化
    @Test
    @Parameters({"value", "isEven"})
    public void givenNumberFromXML_ifEvenCheckOK_thenCorrect(int value, boolean isEven) {
     Assert.assertEquals(isEven, value % 2 == 0);
    }
    

XML 配置:

<suite name="My test suite">
    <test name="numbersXML">
        <parameter name="value" value="1"/>
        <parameter name="isEven" value="false"/>
        <classes>
            <class name="baeldung.com.ParametrizedTests"/>
        </classes>
    </test>
</suite>
  1. @DataProvider 注解(支持复杂数据): ```java @DataProvider(name = "numbers") public static Object[][] evenNumbers() { return new Object[][]{{1, false}, {2, true}, {4, true}}; }

@Test(dataProvider = "numbers") public void givenNumberFromDataProvider_ifEvenCheckOK_thenCorrect (Integer number, boolean expected) { Assert.assertEquals(expected, number % 2 == 0); }


对象参数化示例:
```java
@Test(dataProvider = "numbersObject")
public void givenNumberObjectFromDataProvider_ifEvenCheckOK_thenCorrect
  (EvenNumber number) {
    Assert.assertEquals(number.isEven(), number.getValue() % 2 == 0);
}

@DataProvider(name = "numbersObject")
public Object[][] parameterProvider() {
    return new Object[][]{{new EvenNumber(1, false)},
      {new EvenNumber(2, true)}, {new EvenNumber(4, true)}};
}

✅ TestNG 的 @DataProvider 方法无需静态,可在同一测试类中使用多个数据提供者。

7. 超时测试

超时测试指在指定时间内未完成的测试应标记为失败。

JUnit 5 使用 assertTimeout

@Test
void givenExecution_takeMoreTime_thenFail() throws InterruptedException {
    Assertions.assertTimeout(Duration.ofMillis(1000), () -> Thread.sleep(10000));
}

JUnit 4 和 TestNG 通过 @Test(timeout=...) 实现:

@Test(timeOut = 1000)
public void givenExecution_takeMoreTime_thenFail() {
    while (true);
}

8. 依赖测试

TestNG 支持依赖测试机制:当初始测试失败时,后续依赖测试会被跳过(而非标记为失败)。

典型场景:先验证邮箱有效性,成功后再执行登录:

@Test
public void givenEmail_ifValid_thenTrue() {
    boolean valid = email.contains("@");
    Assert.assertEquals(valid, true);
}

@Test(dependsOnMethods = {"givenEmail_ifValid_thenTrue"})
public void givenValidEmail_whenLoggedIn_thenTrue() {
    LOGGER.info("Email {} valid >> logging in", email);
}

❌ JUnit 不支持原生依赖测试,需通过测试顺序或条件注解模拟。

9. 测试执行顺序

默认情况下,两个框架的测试方法执行顺序均由 Java 反射 API 决定,无固定顺序。JUnit 4 后采用更确定性但不可预测的顺序。

JUnit 5 通过 @TestMethodOrder 控制顺序

@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class SortedUnitTest {
    
    @Test
    @Order(2)
    void a_givenString_whenChangedtoInt_thenTrue() {
        assertTrue(Integer.valueOf("10") instanceof Integer);
    }

    @Test
    @Order(1)
    void b_givenInt_whenChangedtoString_thenTrue() {
        assertTrue(String.valueOf(10) instanceof String);
    }
}

JUnit 4 使用 @FixMethodOrder

@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class SortedUnitTest {

    @Test
    public void a_givenString_whenChangedtoInt_thenTrue() {
        assertTrue(Integer.valueOf("10") instanceof Integer);
    }

    @Test
    public void b_givenInt_whenChangedtoString_thenTrue() {
        assertTrue(String.valueOf(10) instanceof String);
    }
}

TestNG 通过 priority 参数控制

@Test(priority = 1)
public void givenString_whenChangedToInt_thenCorrect() {
    Assert.assertTrue(Integer.valueOf("10") instanceof Integer);
}

@Test(priority = 2)
public void givenInt_whenChangedToString_thenCorrect() {
    Assert.assertTrue(String.valueOf(23) instanceof String);
}

⚠️ 注意:priority 仅控制执行顺序,不保证同一优先级测试完成后再执行下一优先级。严格依赖关系应使用 dependsOnMethods

10. 自定义测试名称

JUnit 5 支持通过 @DisplayName 自定义测试名称,提升报告可读性:

@ParameterizedTest
@ValueSource(strings = { "Hello", "World" })
@DisplayName("Test Method to check that the inputs are not nullable")
void givenString_TestNullOrNot(String word) {
    assertNotNull(word);
}

❌ TestNG 当前不支持自定义测试名称功能。

11. 总结

JUnit 和 TestNG 都是 Java 生态中成熟的测试工具,各有优势:

特性 JUnit TestNG
初始化/清理 方法级和类级 方法级、类级、套件级、分组级
忽略测试 @Disabled/@Ignore @Test(enabled=false)
测试套件 注解驱动 (@Suite) XML 文件驱动
参数化测试 多种内置数据源注解 @Parameters@DataProvider
超时测试 assertTimeout@Test(timeout) @Test(timeOut)
依赖测试 不支持原生 dependsOnMethods
执行顺序控制 @Order@FixMethodOrder priority 参数
自定义测试名称 @DisplayName 不支持

选择建议

  • 简单单元测试:JUnit 5 更轻量级,社区支持更广
  • 复杂测试场景:TestNG 在依赖测试、分组执行、参数化等方面更灵活
  • 遗留项目:JUnit 4 仍广泛使用,但建议升级到 JUnit 5

所有代码示例可在以下仓库找到:


原始标题:A Quick JUnit vs TestNG Comparison | Baeldung

» 下一篇: Apache BVal 介绍