Skip to content

TestNG 概览与对比 ​

T1-015选修进阶约 90 分钟JDK 21测试专项 A:单元测试 · 测试框架对比

1. 本节定位 ​

2026 年的 Java 新项目几乎都用 JUnit 5,那为什么还要了解 TestNG?

三个理由:

  1. 存量项目:很多 2015 年前后启动的项目用 TestNG,你会读到、会维护它;
  2. 部分团队的选择:TestNG 的"分组"与"依赖测试"在某些场景下确实更好用;
  3. 面试:被问到"TestNG 和 JUnit 的区别"是常见题。

本节的目标不是让你会写 TestNG,而是看懂它、能对照、知道该选哪个。

2. 学习目标 ​

完成本节后,你应该能够:

  • 写出 TestNG 的基本测试(生命周期、参数化、异常断言、分组、依赖);
  • 把 JUnit 5 的写法一对一映射到 TestNG,反之亦然;
  • 说出 TestNG 的三个独有能力,以及它们适合什么场景;
  • 说出"新项目该选 JUnit 5"的三个理由;
  • 说明为什么同一个模块不该混用两个测试框架;
  • 给出存量的 TestNG 项目的迁移建议。

3. 前置知识检查 ​

  • 会用 JUnit 5 的生命周期与参数化吗?(T1-002)
  • 知道 surefire 的命名约定吗?(T1-014)

4. 为什么需要这个知识 ​

你会遇到"一半 JUnit、一半 TestNG"的项目。看到这样的代码时,如果不知道两边的对应关系,会陷入"这个注解是干嘛的"的无谓停顿:

java
// TestNG 风格
@BeforeMethod
public void setUp() { }              // 对应 JUnit 5 的 @BeforeEach

@Test(dataProvider = "cases")
public void check(long input, long expected) { }    // 对应 @ParameterizedTest

本节的对照表就是为这个场景准备的——不需要背,只需要能查。

5. 核心概念 ​

5.1 完整对照表 ​

功能JUnit 5TestNG
测试方法@Test@Test
方法级前置@BeforeEach@BeforeMethod
方法级后置@AfterEach@AfterMethod
类级前置@BeforeAll(必须 static)@BeforeClass(无需 static)
类级后置@AfterAll(必须 static)@AfterClass(无需 static)
套件级JUnit Platform Suite@BeforeSuite / @AfterSuite
禁用测试@Disabled@Test(enabled = false)
显示名@DisplayNamedescription 参数(@Test(description="..."))
分组 / 标签@Tag("slow")@Test(groups = "slow")
参数化@ParameterizedTest + 各类 Source@Test(dataProvider = "名字") + @DataProvider
期望异常assertThrows@Test(expectedExceptions = X.class) 或 assertThrows
超时@Timeout / assertTimeout@Test(timeOut = 1000)
依赖测试不支持dependsOnMethods / dependsOnGroups
嵌套测试@Nested不支持
软断言AssertJ 的 SoftAssertions内置 SoftAssert
并行执行配置文件testng.xml 的 parallel 属性
套件配置构建工具 / JUnit Platformtestng.xml(核心特性)

三条最需要注意的差异:

  1. @BeforeClass 不需要 static——从 JUnit 5 过来的人会习惯性加 static(加了也能跑,但语义不同);
  2. 参数化用 Object[][],而不是 JUnit 的注解矩阵;
  3. dependsOnMethods 是 TestNG 独有的(JUnit 5 没有等价功能,见 5.3)。

5.2 完整的 TestNG 示例 ​

文件:labs/testing-testng/src/test/java/cn/kb/learn/testng/TestNgBasicsTest.java(节选)

java
public class TestNgBasicsTest {
    private PayableCalculator calculator;

    @BeforeClass
    public void beforeClass() {
        System.out.println("[TestNG] @BeforeClass 执行(整个类一次)");
    }

    @BeforeMethod
    public void setUp() {
        calculator = new PayableCalculator();          // 每个测试前重建
    }

    @Test
    public void addsUpWithoutDiscount() {
        assertEquals(calculator.payableInCents(5_000L, 1), 5_000L, "未满减时应等于单价");
    }

    @Test(expectedExceptions = IllegalArgumentException.class,
            expectedExceptionsMessageRegExp = "数量必须为正")
    public void rejectsZeroQuantityByAnnotation() {
        calculator.payableInCents(100L, 0);
    }

    @Test
    public void rejectsNegativeUnitPriceByAssertThrows() {
        assertThrows(IllegalArgumentException.class, () -> calculator.payableInCents(-1L, 1));
    }

    @Test(dataProvider = "payableCases")
    public void payableCases(long unitPrice, int quantity, long expected) {
        assertEquals(calculator.payableInCents(unitPrice, quantity), expected);
    }

    @DataProvider
    public Object[][] payableCases() {
        return new Object[][]{
                {5_000L, 1, 5_000L},
                {5_000L, 2, 9_000L},
                {9_999L, 1, 9_999L},
                {10_000L, 1, 9_000L},
                {20_000L, 1, 19_000L}
        };
    }

    @Test(groups = "slow")
    public void groupedAsSlow() {
        assertTrue(true, "被标记为 slow 分组,可用 -Dgroups=slow 单独执行");
    }

    @Test(dependsOnMethods = "addsUpWithoutDiscount")
    public void dependsOnAnotherTest() {
        assertTrue(beforeClassCount > 0, "@BeforeClass 已执行");
    }
}

运行命令与输出:

powershell
cd labs
./mvnw -pl testing-testng test
text
[INFO] Tests run: 11, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.299 s
  -- in cn.kb.learn.testng.TestNgBasicsTest
[INFO] Analyzed bundle 'labs-testing-testng' with 1 classes
[INFO] BUILD SUCCESS

11 个测试是怎么来的:

来源数量
4 个独立 @Test4
@DataProvider 的 5 行数据5
@Test(groups="slow")1
@Test(dependsOnMethods=...)1
合计11

只跑分组:

powershell
./mvnw -pl testing-testng test -Dgroups=slow
text
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS

11 个测试里只有 1 个执行了 ✓ 与 JUnit 5 的 -Dgroups=slow(T1-014 的 5.7 节)行为一致——这是两个框架少有的"参数名相同"的地方(都遵循 surefire 的约定)。

5.3 TestNG 的三个独有能力 ​

能力一:依赖测试(dependsOnMethods) ​

java
@Test
public void createUser() { ... }

@Test(dependsOnMethods = "createUser")
public void updateUser() { ... }      // 上一个失败时,这个被"跳过"而不是"失败"

它解决的问题:把"连锁失败"变成"单一失败"。

举例:一个工作流有 5 步,每步一个测试。step1 失败后,step2 到 step5 都会因为缺少前置数据而失败——报告上 5 个红,实际只有 1 个问题。

用 dependsOnMethods 后,报告显示"1 个失败、4 个跳过"——问题定位时间从 10 分钟降到 10 秒。

为什么 JUnit 5 不提供:JUnit 的哲学是测试之间必须完全独立(T1-007 的 I)。依赖测试与"独立"是冲突的——JUnit 选择从语言层面不给你这个机会。

该不该用:

场景建议
有真实顺序依赖的工作流(如"下单 → 支付 → 退款")可以考虑(但要清楚这是妥协)
只是为了"共享数据"不要用——用 @BeforeMethod 各自准备
单元测试不要用——单元测试就该独立

JUnit 5 里的替代方案:用 @Order + @TestMethodOrder(但那只保证顺序,不提供"跳过"语义)。

能力二:testng.xml 套件配置 ​

xml
<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd">
<suite name="回归套件" parallel="methods" thread-count="4">
  <test name="冒烟">
    <groups>
      <run><include name="smoke"/></run>
    </groups>
    <classes>
      <class name="cn.kb.learn.testng.TestNgBasicsTest"/>
    </classes>
  </test>
</suite>

它能做的事:在不改代码的前提下控制:

  • 跑哪些类、哪些分组;
  • 执行顺序与依赖;
  • 并行策略(methods / classes / tests 级别 + 线程数);
  • 参数注入(<parameter name="env" value="staging"/>)。

为什么这在 UI 自动化与接口自动化里特别有用(TB 测试专项):同一套测试代码,用不同的 XML 组合成"冒烟""回归""全量"三种执行策略。

JUnit 5 的对应方案:用构建工具的多 profile(Maven profile + -Dgroups)、或 JUnit Platform Suite(@Suite)——表达力不如 testng.xml。

能力三:内置软断言 ​

java
SoftAssert softAssert = new SoftAssert();
softAssert.assertEquals(actual1, expected1, "字段 1");
softAssert.assertEquals(actual2, expected2, "字段 2");
softAssert.assertAll();                 // 一次性报告所有失败

JUnit 5 的对应方案:用 AssertJ 的 SoftAssertions(T1-009)——功能相当,所以这一条不构成选型理由。

5.4 为什么新项目选 JUnit 5 ​

理由说明
生态与集成Spring Boot 的 spring-boot-starter-test 默认带 JUnit 5;绝大多数测试库(Mockito、AssertJ、Testcontainers)以 JUnit 5 为一等公民
工具支持IDE 的测试运行器、构建工具、CI 报告解析都以 JUnit Platform 为基准
持续演进JUnit 5 的分层架构(Platform / Jupiter / Vintage)让它能同时跑 JUnit 4 与 5 的测试,迁移友好
社区规模问题、教程、扩展的数量级差异

TestNG 仍然活跃,但新项目的默认选择已经是 JUnit 5。

5.5 不要混用(本节最重要的实践建议) ​

同一个模块里同时放 JUnit 5 与 TestNG 的测试会出问题:

问题说明
surefire 只选一个 providerMaven 的 surefire 会根据依赖自动选择测试框架,两个都在时可能只有一种测试被执行
生命周期语义不一致@BeforeEach 与 @BeforeMethod 混在同一批测试里,行为难预测
报告混杂两个框架的报告格式不同,CI 解析困难

本项目的做法:独立模块(labs/testing-testng)——不与 labs/testing-unit 混用。

如果你必须在一个工程里同时支持:用多模块(每个框架一个模块),或者配置 surefire 的 <providers>(复杂且脆弱,不推荐)。

5.6 迁移建议 ​

如果你接手一个 TestNG 项目:

建议说明
不要为了迁移而迁移能跑、能维护的测试就是好测试——迁移本身不产生价值
新测试用 JUnit 5(新模块)新代码用新框架,老代码不动
随手迁移当你因为改需求而必须动某个测试类时,顺手把它迁到 JUnit 5
先统一断言库把 org.testng.Assert 换成 AssertJ——这一步的收益最大(断言质量提升),且与框架无关
迁移时优先保行为先把测试跑绿,再考虑优化命名与结构

一句话:渐进式迁移,不搞"大爆炸"重写。

6. 递进示例 ​

6.1 最小示例:参数化 ​

java
@Test(dataProvider = "payableCases")
public void payableCases(long unitPrice, int quantity, long expected) {
    assertEquals(calculator.payableInCents(unitPrice, quantity), expected);
}

@DataProvider
public Object[][] payableCases() {
    return new Object[][]{
            {5_000L, 1, 5_000L},
            {5_000L, 2, 9_000L}
    };
}

与 JUnit 5 的对比:

JUnit 5TestNG
数据源@ValueSource / @CsvSource / @MethodSource只有 @DataProvider
声明方式注解内联(@CsvSource({"1,2"}))方法返回 Object[][]
显示名name = "单价 {0} × {1}"无占位符机制(只能靠 description)

评价:JUnit 5 的内联写法更紧凑,TestNG 的 Object[][] 更灵活(可以在方法里写循环生成数据)。各有取舍。

6.2 业务示例:分组执行 ​

见 5.2 节的输出:-Dgroups=slow 只跑 1 个测试。

两边的对应:

JUnit 5TestNG
标记@Tag("slow")@Test(groups = "slow")
只跑-Dgroups=slow-Dgroups=slow
排除-DexcludedGroups=slow-DexcludedGroups=slow

命令完全一样——因为这是 surefire 的参数,不是框架的参数。

6.3 反例:把"依赖测试"用在单元测试上 ​

java
@Test
public void createUser() { ... }

@Test(dependsOnMethods = "createUser")
public void updateUser() { ... }

问题:如果这两个是单元测试,那它们本来就不该有依赖(T1-007 的 Independent)。

什么时候才该用:集成测试或端到端流程——那里"步骤顺序"是真实的业务约束("必须先创建订单才能支付")。

判断标准:这个顺序是"业务的顺序"还是"你为了方便而造的顺序"?

7. 完整可运行代码 ​

模块结构(独立模块,避免框架混用):

text
labs/testing-testng/
├── pom.xml                                    # 只依赖 org.testng:testng
└── src/
    ├── main/java/cn/kb/learn/testng/PayableCalculator.java
    └── test/java/cn/kb/learn/testng/TestNgBasicsTest.java

POM 关键部分:

xml
<dependencies>
  <dependency>
    <groupId>org.testng</groupId>
    <artifactId>testng</artifactId>
    <version>7.10.2</version>
    <scope>test</scope>
  </dependency>
</dependencies>

运行命令:

powershell
cd labs
./mvnw -pl testing-testng test              # 全部 11 个测试
./mvnw -pl testing-testng test -Dgroups=slow   # 只跑 slow 分组

预期输出:

text
[INFO] Tests run: 11, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.299 s
  -- in cn.kb.learn.testng.TestNgBasicsTest
[INFO] Analyzed bundle 'labs-testing-testng' with 1 classes
[INFO] BUILD SUCCESS

分组执行:

text
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS

两处值得注意:

观察含义
surefire 自动识别了 TestNG因为这个模块只有 TestNG 依赖;两个框架同时存在时行为不确定(5.5 节)
命令参数与 JUnit 5 完全一致-Dgroups / -DexcludedGroups 是 surefire 的参数,与框架无关

8. Debug 观察点 ​

  1. 在 @BeforeClass 与 @BeforeMethod 里各打一个断点,观察执行次数(与 JUnit 5 的 @BeforeAll / @BeforeEach 完全一致);
  2. 在 dependsOnAnotherTest 里断点,然后故意让 addsUpWithoutDiscount 失败(改断言),重跑——观察 dependsOnAnotherTest 的状态是 SKIPPED 而不是 FAILED;
  3. 在 @DataProvider 的 payableCases() 方法里断点,观察它在测试开始前被调用一次,返回 5 行数据;
  4. 用 -Dgroups=slow 运行时观察只有 1 个测试被执行,其余的既不是失败也不是跳过(因为根本没被选中)。

9. 常见错误 ​

错误 1:同一个模块混用两个框架 ​

症状:加了 TestNG 依赖后,JUnit 5 的测试突然不跑了(或反过来)。

原因:surefire 只能选一个测试 provider。

修复:分模块(本项目的做法),或者只用一种框架。

错误 2:以为 @BeforeClass 也必须 static ​

java
@BeforeClass
public static void beforeClass() { }      // 可以,但不需要

说明:TestNG 的测试类实例默认每个类创建一个(不是每个方法)——这与 JUnit 5 的"每方法一新实例"语义不同。

后果:TestNG 里实例字段会在所有测试间共享,需要自己用 @BeforeMethod 重置(本示例就是这么做的)。

这是从 JUnit 5 迁到 TestNG 时最容易踩的坑——"测试之间共享实例状态"在 TestNG 里是默认行为。

错误 3:把 TestNG 的 assert 当成 JUnit 的 ​

java
import static org.testng.Assert.assertEquals;

注意参数顺序:TestNG 的 assertEquals(actual, expected) 与 JUnit 5 相同(都是实际值在前)——但消息参数的位置不同:

java
// JUnit 5:消息在最后
assertEquals(expected, actual, "消息");

// TestNG:消息也可以最后,但 assertEquals(actual, expected, message) 的顺序是"实际、期望、消息"
assertEquals(actual, expected, "消息");

风险:JUnit 5 的 assertEquals(expected, actual) 与 TestNG 的 assertEquals(actual, expected) 恰好相反。

修复:改用 AssertJ(assertThat(actual).isEqualTo(expected)),彻底绕开参数顺序问题(T1-009 讲过 JUnit 那边的同类问题)。

错误 4:用依赖测试掩盖"测试不独立" ​

见 6.3 节。修复:先问"这个依赖是业务的还是我的"。

错误 5:为了"更强大"而选 TestNG ​

"TestNG 有依赖测试、有 testng.xml,所以更强" —— 这是一个常见的误判。

能力更强不等于更适合:JUnit 5 故意不提供依赖测试,因为它鼓励的设计是"测试完全独立"(T1-007)。一个你不需要的能力,不构成选型理由。

选型应该看:团队熟悉度、生态兼容性、工具链支持——这三项 JUnit 5 都占优。

10. 动手实验 ​

目标:把同一个测试用两个框架各写一遍,体会差异。

步骤:

  1. 运行 ./mvnw -pl testing-testng test 与 -Dgroups=slow,核对两组输出;
  2. 把 TestNgBasicsTest 逐行翻译成 JUnit 5(放在 labs/testing-unit 里,不要放在 testng 模块),对照点:
    • @BeforeClass → @BeforeAll(注意加 static);
    • @BeforeMethod → @BeforeEach;
    • @DataProvider → @MethodSource;
    • @Test(expectedExceptions = X.class) → assertThrows(X.class, ...);
    • @Test(groups = "slow") → @Tag("slow");
  3. 数一数:两边的行数差多少?哪个更简洁?
  4. dependsOnMethods 这一条在 JUnit 5 里怎么写? 试着用 @Order 实现"顺序",再想"跳过"语义该怎么实现;
  5. 给两边各加一条 @DisplayName / description,对比报告的可读性。

验收方式:能说出第 4 步的关键差异(JUnit 5 没有等价的"跳过"语义)。

扩展任务:写一个 testng.xml,把 TestNgBasicsTest 配成"只跑 slow 分组 + 方法级并行 4 线程",跑一次并观察执行日志里的线程名。

11. 练习题 ​

基础题

  1. 判断:TestNG 的 @BeforeClass 必须是静态方法。(对 / 错)
  2. 填空:TestNG 的参数化用 ______ 提供数据;依赖测试用 ______ 声明。
  3. 简答:为什么同一个模块里不应该同时放 JUnit 5 与 TestNG 的测试?

代码阅读题

java
public class DemoTest {
    private int counter = 0;

    @BeforeMethod
    public void setUp() {
        counter++;
    }

    @Test
    public void first() {
        assertTrue(counter >= 1);
    }

    @Test
    public void second() {
        assertTrue(counter >= 1);
    }
}
  1. second 里的 counter 可能是多少?为什么?
  2. 如果这是 JUnit 5 的代码,答案会不同吗?

编程题

  1. 用 TestNG 重写一个 JUnit 5 测试:
    • 取 T1-002 的 ParameterizedOrderCalculatorTest;
    • 用 @DataProvider 替换 @CsvSource 与 @MethodSource;
    • 用 @Test(expectedExceptions = ...) 替换一处 assertThrows;
    • 用 groups 替换 @Tag;
    • 跑通后,写出两版的对比结论(代码量、可读性、可维护性);
    • 说明:如果团队要维护这个测试 3 年,你会选哪个版本?为什么?

综合任务

  1. 为一个"存量 TestNG 项目"设计迁移方案:
    • 假设有 200 个 TestNG 测试类,分布在 10 个模块;
    • 设计迁移的优先级(哪些先迁、哪些不迁);
    • 设计"新旧共存"的工程结构(怎么避免框架混用);
    • 设计验收标准(迁完之后怎么确认"行为没有变化");
    • 估算工作量与风险,说明"什么情况下你会建议不迁移";
    • 额外要求:列出"迁移过程中最容易丢失的东西"(提示:TestNG 的 @BeforeMethod 与 JUnit 5 的 @BeforeEach 在实例生命周期上有差异)。
分级提示

提示 1(方向):第 4 题的关键是 TestNG 的测试类实例在方法间共享(与 JUnit 5 相反)。

提示 2(关键点):第 6 题的 @DataProvider 需要把数据写成 Object[][],泛型与装箱要注意。

提示 3(接近答案):第 7 题的"最容易丢失的东西"包括:@Test(timeOut) 的超时语义、expectedExceptionsMessageRegExp、依赖测试的跳过语义、分组配置。每一条都要在迁移检查清单里。

12. 参考答案 ​

基础题
  1. 错。TestNG 的 @BeforeClass 可以是实例方法(不需要 static),因为 TestNG 的测试类实例默认在类级别创建(所有测试方法共享同一个实例)。

注意这带来一个语义差异:JUnit 5 的"每个测试方法一个新实例"(T1-002)在 TestNG 里不成立——实例字段会在所有测试间共享,需要自己用 @BeforeMethod 重置。

  1. @DataProvider;dependsOnMethods(或 dependsOnGroups)。
  2. 因为 surefire 只能选择一个测试 provider——两个框架同时存在于一个模块时,可能出现"另一种框架的测试完全不执行"的情况(而且不一定报错)。

更糟的是:这类问题在本地可能是"只有一半测试在跑",而 CI 上因为环境差异表现得不一样,排查成本很高。修复方式:分模块,或用 Maven profile 切换。

代码阅读题(第 4、5 题)

第 4 题:counter 可能是 1 或 2(取决于 first 有没有先跑)。

原因:TestNG 的测试类实例在类级别创建一次,所以 counter 是实例字段,在两个测试间共享:

执行顺序first 里的 countersecond 里的 counter
first → second12
second → first2(second 先跑:1,但断言 >= 1 通过)1

这个测试能通过是因为断言写的是 >= 1——如果写的是 == 1,那第二个跑的测试就会失败。

第 5 题:如果这是 JUnit 5,答案不同。

JUnit 5 默认每个测试方法创建一个新实例,所以:

执行顺序first 里的 countersecond 里的 counter
任意顺序11

每个测试都从"全新实例"开始——这就是 JUnit 5 的隔离保证(T1-002 的核心)。

这个对比题的价值:它是从 JUnit 5 迁移到 TestNG 时最容易踩的坑——原来靠"每方法新实例"保证的隔离,在 TestNG 里需要显式写 @BeforeMethod 重置字段。迁移时如果漏了这一步,测试之间就会互相污染(而症状是"偶发失败",最难排查)。

编程题(第 6 题)
java
package cn.kb.learn.testng;

import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertThrows;

@Test(groups = "unit")
public class ParameterizedPayableTest {
    @Test(dataProvider = "payableCases")
    public void payableCases(long unitPrice, int quantity, long expected) {
        assertEquals(new PayableCalculator().payableInCents(unitPrice, quantity), expected);
    }

    @DataProvider
    public Object[][] payableCases() {
        return new Object[][]{
                {5_000L, 1, 5_000L},
                {5_000L, 2, 9_000L},
                {9_999L, 1, 9_999L},
                {10_000L, 1, 9_000L},
                {20_000L, 1, 19_000L}
        };
    }

    @Test(expectedExceptions = IllegalArgumentException.class,
            expectedExceptionsMessageRegExp = "数量必须为正")
    public void rejectsZeroQuantity() {
        new PayableCalculator().payableInCents(100L, 0);
    }

    @Test
    public void rejectsNegativeQuantity() {
        assertThrows(IllegalArgumentException.class,
                () -> new PayableCalculator().payableInCents(100L, -1));
    }
}

两版对比结论:

维度JUnit 5TestNG
参数化代码量更少(@CsvSource 内联 5 行数据)更多(要单独写 @DataProvider 方法 + Object[][])
数据生成的灵活性受限于注解可以在 DataProvider 里写循环
显示名name = "单价 {0} × {1}" 占位符无等价机制(报告里只能看到参数值)
异常断言assertThrows 一种写法两种(注解 / assertThrows)
分组@Taggroups 属性(可在类级统一声明)
类型安全编译期检查参数类型Object[][] 容易写错顺序(如把 quantity 与 expected 写反)

如果团队要维护 3 年,我会选 JUnit 5,理由:

  1. 类型安全:@CsvSource 的参数会被映射到方法参数类型,写错会有编译或运行时的明确报错;而 Object[][] 里"参数顺序写反"是静默的错误(测试可能照常通过,因为类型能自动装箱);
  2. 显示名:name = "单价 {0} × {1} 件 → 应付 {2} 分" 让报告可读,TestNG 没有等价能力——排查失败时这很关键;
  3. 生态:3 年内团队大概率会用 Spring Boot、Testcontainers、AssertJ——它们都以 JUnit 5 为主。

唯一会让我选 TestNG 的情况:团队已经重度使用 testng.xml 做多套件管理(比如 UI 自动化里有"冒烟/回归/全量"三套 XML),迁移成本高于收益。

综合任务(第 7 题)

迁移方案:

markdown
## 一、迁移优先级

| 优先级 | 类别 | 理由 |
|-------|------|------|
| P0(先迁) | **新增的测试** | 直接用 JUnit 5,零迁移成本 |
| P1 | 经常修改的测试类 | 每次改需求时顺手迁移 |
| P2 | 稳定的核心业务测试 | 迁移收益中等,风险可控 |
| **不迁** | 稳定且很少改动的边缘测试 | **迁移不产生价值**,风险大于收益 |
| **不迁** | 重度依赖 testng.xml 的套件 | 迁移成本极高(并行策略、参数注入都要重做) |

## 二、工程结构(避免框架混用)

推荐"分模块 + 渐进替换":

project/ ├── module-a/ # 保留 TestNG(含 testng.xml) ├── module-b/ # 保留 TestNG └── module-new/ # JUnit 5(新代码都放这里)


**不要**在同一模块里同时放两个框架(surefire 只选一个 provider)。

如果必须同模块共存:用 Maven profile 切换 surefire 的 provider 配置,**但这是脆弱方案**。

## 三、验收标准

| 检查项 | 方式 |
|-------|------|
| 测试数量一致 | 迁移前后对比 `Tests run` 数字 |
| 覆盖的场景一致 | 用覆盖率报告对比(分支覆盖不下降) |
| 断言强度不降低 | 人工审查:原来有断言的,迁移后仍有 |
| 边界用例仍在 | 逐个检查参数化数据的行数 |
| 执行时间未显著变长 | 对比 CI 的测试阶段耗时 |

## 四、工作量与风险

- 工作量:**每个测试类约 0.5-2 小时**(200 个类 ≈ 100-400 小时)
- 风险:
  - 迁移过程中引入的"测试写错"(比不迁移更危险)
  - 依赖测试的跳过语义丢失
  - 并行策略需要重做

## 五、什么情况下建议不迁移

1. **项目接近生命周期的末期**(一年内下线);
2. **测试稳定、很少改动**(迁移收益接近零);
3. **重度依赖 testng.xml 的复杂套件管理**;
4. **团队对 TestNG 非常熟练且没有引入 JUnit 5 的必要**。

**核心判断**:迁移的目的应该是"**让新代码用上生态更好的框架**",而不是"消灭 TestNG"。

## 六、迁移中最容易丢失的东西(检查清单)

| 容易丢的 | 说明 | 对策 |
|---------|------|------|
| **实例生命周期语义** | TestNG 类级实例 vs JUnit 5 方法级实例 | 迁移后**每个测试都要能独立运行**(重点验证) |
| `@Test(timeOut)` | 超时断言可能被漏掉 | 逐个检查并换成 `assertTimeout` |
| `expectedExceptionsMessageRegExp` | 消息正则校验 | 换成 `assertThrows` + `hasMessageMatching` |
| **依赖测试的跳过语义** | JUnit 5 没有等价功能 | 用 `@Order` 只能保证顺序,**不能跳过**——需要重新设计这些测试 |
| **分组配置** | `groups` → `@Tag` | 检查 surefire 的 `-Dgroups` 参数是否仍能选中 |
| `@BeforeSuite` / `@AfterSuite` | 套件级钩子 | JUnit 5 用 `@BeforeAll`(在 Suite 类里)或扩展 |
| `testng.xml` 的并行配置 | 方法级并行 | JUnit 5 需要在 `junit-platform.properties` 里配 |

**最重要的一条**:**"实例生命周期语义"**。

它是**唯一一个"迁移后代码看起来没问题、但行为悄悄变了"的差异**——TestNG 的实例共享可能让某些测试之间有隐式的顺序依赖,迁到 JUnit 5 后(每方法新实例)这些依赖消失了,**测试可能从"偶然通过"变成"稳定通过"(好事),也可能从"通过"变成"失败"(暴露原本就存在的问题)**。

**建议**:**迁移时每个测试类都单独运行一遍**(不只是跑整个套件),这样能发现隐式的相互依赖。

13. 掌握标准 ​

  • [ ] 能写出 TestNG 的基本测试(生命周期、参数化、异常、分组、依赖);
  • [ ] 能对照两边的注解,看懂 TestNG 的老代码;
  • [ ] 能说出 TestNG 的三个独有能力,以及 JUnit 5 的对应替代方案;
  • [ ] 知道"新项目选 JUnit 5"的三个理由;
  • [ ] 知道不能在同一模块混用两个框架,以及为什么;
  • [ ] 知道 TestNG 的实例生命周期与 JUnit 5 相反,这是迁移的最大陷阱。

14. 扩展资源 ​

15. 下一步学习 ​

TA 单元测试专项可写部分全部完成。

剩余两节依赖 Spring 章节:

接下来建议进入 S3 企业开发基础:MySQL、Spring、Git 与 Linux(该阶段页面将在产出时建立)——完成后回来补这两节,TA 就完整了。

进度只保存在本机浏览器,可随时清除

个人学习项目:AI 生成内容均经人工复核,第三方资源仅作索引