主题
界面
主题
2026 年的 Java 新项目几乎都用 JUnit 5,那为什么还要了解 TestNG?
三个理由:
本节的目标不是让你会写 TestNG,而是看懂它、能对照、知道该选哪个。
完成本节后,你应该能够:
你会遇到"一半 JUnit、一半 TestNG"的项目。看到这样的代码时,如果不知道两边的对应关系,会陷入"这个注解是干嘛的"的无谓停顿:
// TestNG 风格
@BeforeMethod
public void setUp() { } // 对应 JUnit 5 的 @BeforeEach
@Test(dataProvider = "cases")
public void check(long input, long expected) { } // 对应 @ParameterizedTest本节的对照表就是为这个场景准备的——不需要背,只需要能查。
| 功能 | JUnit 5 | TestNG |
|---|---|---|
| 测试方法 | @Test | @Test |
| 方法级前置 | @BeforeEach | @BeforeMethod |
| 方法级后置 | @AfterEach | @AfterMethod |
| 类级前置 | @BeforeAll(必须 static) | @BeforeClass(无需 static) |
| 类级后置 | @AfterAll(必须 static) | @AfterClass(无需 static) |
| 套件级 | JUnit Platform Suite | @BeforeSuite / @AfterSuite |
| 禁用测试 | @Disabled | @Test(enabled = false) |
| 显示名 | @DisplayName | description 参数(@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 Platform | testng.xml(核心特性) |
三条最需要注意的差异:
@BeforeClass 不需要 static——从 JUnit 5 过来的人会习惯性加 static(加了也能跑,但语义不同);Object[][],而不是 JUnit 的注解矩阵;dependsOnMethods 是 TestNG 独有的(JUnit 5 没有等价功能,见 5.3)。文件:labs/testing-testng/src/test/java/cn/kb/learn/testng/TestNgBasicsTest.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 已执行");
}
}运行命令与输出:
cd labs
./mvnw -pl testing-testng test[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 SUCCESS11 个测试是怎么来的:
| 来源 | 数量 |
|---|---|
4 个独立 @Test | 4 |
@DataProvider 的 5 行数据 | 5 |
@Test(groups="slow") | 1 |
@Test(dependsOnMethods=...) | 1 |
| 合计 | 11 |
只跑分组:
./mvnw -pl testing-testng test -Dgroups=slow[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS11 个测试里只有 1 个执行了 ✓ 与 JUnit 5 的 -Dgroups=slow(T1-014 的 5.7 节)行为一致——这是两个框架少有的"参数名相同"的地方(都遵循 surefire 的约定)。
dependsOnMethods) @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 套件配置 <!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>它能做的事:在不改代码的前提下控制:
<parameter name="env" value="staging"/>)。为什么这在 UI 自动化与接口自动化里特别有用(TB 测试专项):同一套测试代码,用不同的 XML 组合成"冒烟""回归""全量"三种执行策略。
JUnit 5 的对应方案:用构建工具的多 profile(Maven profile + -Dgroups)、或 JUnit Platform Suite(@Suite)——表达力不如 testng.xml。
SoftAssert softAssert = new SoftAssert();
softAssert.assertEquals(actual1, expected1, "字段 1");
softAssert.assertEquals(actual2, expected2, "字段 2");
softAssert.assertAll(); // 一次性报告所有失败JUnit 5 的对应方案:用 AssertJ 的 SoftAssertions(T1-009)——功能相当,所以这一条不构成选型理由。
| 理由 | 说明 |
|---|---|
| 生态与集成 | 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。
同一个模块里同时放 JUnit 5 与 TestNG 的测试会出问题:
| 问题 | 说明 |
|---|---|
| surefire 只选一个 provider | Maven 的 surefire 会根据依赖自动选择测试框架,两个都在时可能只有一种测试被执行 |
| 生命周期语义不一致 | @BeforeEach 与 @BeforeMethod 混在同一批测试里,行为难预测 |
| 报告混杂 | 两个框架的报告格式不同,CI 解析困难 |
本项目的做法:独立模块(labs/testing-testng)——不与 labs/testing-unit 混用。
如果你必须在一个工程里同时支持:用多模块(每个框架一个模块),或者配置 surefire 的 <providers>(复杂且脆弱,不推荐)。
如果你接手一个 TestNG 项目:
| 建议 | 说明 |
|---|---|
| 不要为了迁移而迁移 | 能跑、能维护的测试就是好测试——迁移本身不产生价值 |
| 新测试用 JUnit 5(新模块) | 新代码用新框架,老代码不动 |
| 随手迁移 | 当你因为改需求而必须动某个测试类时,顺手把它迁到 JUnit 5 |
| 先统一断言库 | 把 org.testng.Assert 换成 AssertJ——这一步的收益最大(断言质量提升),且与框架无关 |
| 迁移时优先保行为 | 先把测试跑绿,再考虑优化命名与结构 |
一句话:渐进式迁移,不搞"大爆炸"重写。
@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 5 | TestNG | |
|---|---|---|
| 数据源 | @ValueSource / @CsvSource / @MethodSource | 只有 @DataProvider |
| 声明方式 | 注解内联(@CsvSource({"1,2"})) | 方法返回 Object[][] |
| 显示名 | name = "单价 {0} × {1}" | 无占位符机制(只能靠 description) |
评价:JUnit 5 的内联写法更紧凑,TestNG 的 Object[][] 更灵活(可以在方法里写循环生成数据)。各有取舍。
见 5.2 节的输出:-Dgroups=slow 只跑 1 个测试。
两边的对应:
| JUnit 5 | TestNG | |
|---|---|---|
| 标记 | @Tag("slow") | @Test(groups = "slow") |
| 只跑 | -Dgroups=slow | -Dgroups=slow |
| 排除 | -DexcludedGroups=slow | -DexcludedGroups=slow |
命令完全一样——因为这是 surefire 的参数,不是框架的参数。
@Test
public void createUser() { ... }
@Test(dependsOnMethods = "createUser")
public void updateUser() { ... }问题:如果这两个是单元测试,那它们本来就不该有依赖(T1-007 的 Independent)。
什么时候才该用:集成测试或端到端流程——那里"步骤顺序"是真实的业务约束("必须先创建订单才能支付")。
判断标准:这个顺序是"业务的顺序"还是"你为了方便而造的顺序"?
模块结构(独立模块,避免框架混用):
labs/testing-testng/
├── pom.xml # 只依赖 org.testng:testng
└── src/
├── main/java/cn/kb/learn/testng/PayableCalculator.java
└── test/java/cn/kb/learn/testng/TestNgBasicsTest.javaPOM 关键部分:
<dependencies>
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>7.10.2</version>
<scope>test</scope>
</dependency>
</dependencies>运行命令:
cd labs
./mvnw -pl testing-testng test # 全部 11 个测试
./mvnw -pl testing-testng test -Dgroups=slow # 只跑 slow 分组预期输出:
[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分组执行:
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS两处值得注意:
| 观察 | 含义 |
|---|---|
| surefire 自动识别了 TestNG | 因为这个模块只有 TestNG 依赖;两个框架同时存在时行为不确定(5.5 节) |
| 命令参数与 JUnit 5 完全一致 | -Dgroups / -DexcludedGroups 是 surefire 的参数,与框架无关 |
@BeforeClass 与 @BeforeMethod 里各打一个断点,观察执行次数(与 JUnit 5 的 @BeforeAll / @BeforeEach 完全一致);dependsOnAnotherTest 里断点,然后故意让 addsUpWithoutDiscount 失败(改断言),重跑——观察 dependsOnAnotherTest 的状态是 SKIPPED 而不是 FAILED;@DataProvider 的 payableCases() 方法里断点,观察它在测试开始前被调用一次,返回 5 行数据;-Dgroups=slow 运行时观察只有 1 个测试被执行,其余的既不是失败也不是跳过(因为根本没被选中)。症状:加了 TestNG 依赖后,JUnit 5 的测试突然不跑了(或反过来)。
原因:surefire 只能选一个测试 provider。
修复:分模块(本项目的做法),或者只用一种框架。
@BeforeClass 也必须 static @BeforeClass
public static void beforeClass() { } // 可以,但不需要说明:TestNG 的测试类实例默认每个类创建一个(不是每个方法)——这与 JUnit 5 的"每方法一新实例"语义不同。
后果:TestNG 里实例字段会在所有测试间共享,需要自己用 @BeforeMethod 重置(本示例就是这么做的)。
这是从 JUnit 5 迁到 TestNG 时最容易踩的坑——"测试之间共享实例状态"在 TestNG 里是默认行为。
assert 当成 JUnit 的 import static org.testng.Assert.assertEquals;注意参数顺序:TestNG 的 assertEquals(actual, expected) 与 JUnit 5 相同(都是实际值在前)——但消息参数的位置不同:
// 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 那边的同类问题)。
见 6.3 节。修复:先问"这个依赖是业务的还是我的"。
"TestNG 有依赖测试、有 testng.xml,所以更强" —— 这是一个常见的误判。
能力更强不等于更适合:JUnit 5 故意不提供依赖测试,因为它鼓励的设计是"测试完全独立"(T1-007)。一个你不需要的能力,不构成选型理由。
选型应该看:团队熟悉度、生态兼容性、工具链支持——这三项 JUnit 5 都占优。
目标:把同一个测试用两个框架各写一遍,体会差异。
步骤:
./mvnw -pl testing-testng test 与 -Dgroups=slow,核对两组输出;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");dependsOnMethods 这一条在 JUnit 5 里怎么写? 试着用 @Order 实现"顺序",再想"跳过"语义该怎么实现;@DisplayName / description,对比报告的可读性。验收方式:能说出第 4 步的关键差异(JUnit 5 没有等价的"跳过"语义)。
扩展任务:写一个 testng.xml,把 TestNgBasicsTest 配成"只跑 slow 分组 + 方法级并行 4 线程",跑一次并观察执行日志里的线程名。
基础题
@BeforeClass 必须是静态方法。(对 / 错)代码阅读题
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);
}
}second 里的 counter 可能是多少?为什么?编程题
ParameterizedOrderCalculatorTest;@DataProvider 替换 @CsvSource 与 @MethodSource;@Test(expectedExceptions = ...) 替换一处 assertThrows;groups 替换 @Tag;综合任务
@BeforeMethod 与 JUnit 5 的 @BeforeEach 在实例生命周期上有差异)。提示 1(方向):第 4 题的关键是 TestNG 的测试类实例在方法间共享(与 JUnit 5 相反)。
提示 2(关键点):第 6 题的 @DataProvider 需要把数据写成 Object[][],泛型与装箱要注意。
提示 3(接近答案):第 7 题的"最容易丢失的东西"包括:@Test(timeOut) 的超时语义、expectedExceptionsMessageRegExp、依赖测试的跳过语义、分组配置。每一条都要在迁移检查清单里。
@BeforeClass 可以是实例方法(不需要 static),因为 TestNG 的测试类实例默认在类级别创建(所有测试方法共享同一个实例)。注意这带来一个语义差异:JUnit 5 的"每个测试方法一个新实例"(T1-002)在 TestNG 里不成立——实例字段会在所有测试间共享,需要自己用 @BeforeMethod 重置。
@DataProvider;dependsOnMethods(或 dependsOnGroups)。更糟的是:这类问题在本地可能是"只有一半测试在跑",而 CI 上因为环境差异表现得不一样,排查成本很高。修复方式:分模块,或用 Maven profile 切换。
第 4 题:counter 可能是 1 或 2(取决于 first 有没有先跑)。
原因:TestNG 的测试类实例在类级别创建一次,所以 counter 是实例字段,在两个测试间共享:
| 执行顺序 | first 里的 counter | second 里的 counter |
|---|---|---|
| first → second | 1 | 2 |
| second → first | 2(second 先跑:1,但断言 >= 1 通过) | 1 |
这个测试能通过是因为断言写的是 >= 1——如果写的是 == 1,那第二个跑的测试就会失败。
第 5 题:如果这是 JUnit 5,答案不同。
JUnit 5 默认每个测试方法创建一个新实例,所以:
| 执行顺序 | first 里的 counter | second 里的 counter |
|---|---|---|
| 任意顺序 | 1 | 1 |
每个测试都从"全新实例"开始——这就是 JUnit 5 的隔离保证(T1-002 的核心)。
这个对比题的价值:它是从 JUnit 5 迁移到 TestNG 时最容易踩的坑——原来靠"每方法新实例"保证的隔离,在 TestNG 里需要显式写 @BeforeMethod 重置字段。迁移时如果漏了这一步,测试之间就会互相污染(而症状是"偶发失败",最难排查)。
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 5 | TestNG |
|---|---|---|
| 参数化代码量 | 更少(@CsvSource 内联 5 行数据) | 更多(要单独写 @DataProvider 方法 + Object[][]) |
| 数据生成的灵活性 | 受限于注解 | 可以在 DataProvider 里写循环 |
| 显示名 | name = "单价 {0} × {1}" 占位符 | 无等价机制(报告里只能看到参数值) |
| 异常断言 | assertThrows 一种写法 | 两种(注解 / assertThrows) |
| 分组 | @Tag | groups 属性(可在类级统一声明) |
| 类型安全 | 编译期检查参数类型 | Object[][] 容易写错顺序(如把 quantity 与 expected 写反) |
如果团队要维护 3 年,我会选 JUnit 5,理由:
@CsvSource 的参数会被映射到方法参数类型,写错会有编译或运行时的明确报错;而 Object[][] 里"参数顺序写反"是静默的错误(测试可能照常通过,因为类型能自动装箱);name = "单价 {0} × {1} 件 → 应付 {2} 分" 让报告可读,TestNG 没有等价能力——排查失败时这很关键;唯一会让我选 TestNG 的情况:团队已经重度使用 testng.xml 做多套件管理(比如 UI 自动化里有"冒烟/回归/全量"三套 XML),迁移成本高于收益。
迁移方案:
## 一、迁移优先级
| 优先级 | 类别 | 理由 |
|-------|------|------|
| 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 后(每方法新实例)这些依赖消失了,**测试可能从"偶然通过"变成"稳定通过"(好事),也可能从"通过"变成"失败"(暴露原本就存在的问题)**。
**建议**:**迁移时每个测试类都单独运行一遍**(不只是跑整个套件),这样能发现隐式的相互依赖。TA 单元测试专项可写部分全部完成。
剩余两节依赖 Spring 章节:
接下来建议进入 S3 企业开发基础:MySQL、Spring、Git 与 Linux(该阶段页面将在产出时建立)——完成后回来补这两节,TA 就完整了。
暂无