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 提供多种数据源注解:
**
@ValueSource
**:基本类型数组@ParameterizedTest @ValueSource(strings = { "Hello", "World" }) void givenString_TestNullOrNot(String word) { assertNotNull(word); }
**
@EnumSource
**:枚举常量@ParameterizedTest @EnumSource(value = PizzaDeliveryStrategy.class, names = {"EXPRESS", "NORMAL"}) void givenEnum_TestContainsOrNot(PizzaDeliveryStrategy timeUnit) { assertTrue(EnumSet.of(PizzaDeliveryStrategy.EXPRESS, PizzaDeliveryStrategy.NORMAL).contains(timeUnit)); }
**
@MethodSource
**:外部方法生成数据流 ```java static StreamwordDataProvider() { 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
实现参数化:
- 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>
@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
所有代码示例可在以下仓库找到: