原创内容,未获授权禁止转载、转发、抄袭。
Java 自动化测试集逐渐增大后,通常需要按场景分组、批量注入数据、控制执行依赖和并行调度。TestNG 把这些能力直接放进测试框架,适合组织单元测试、接口测试和回归测试。
本文使用一个最小的内存"取消订单"案例,集中演示 TestNG 的生命周期、断言、@DataProvider、分组、testng.xml 和并行执行,并说明依赖测试与共享状态最容易踩的坑。
TestNG 适合解决什么问题
| 能力 | 典型用途 |
|---|---|
| 分组 | 区分冒烟、回归、接口和慢测试 |
@DataProvider |
用多组输入批量验证同一业务规则 |
testng.xml |
组合测试类、分组、参数和并行策略 |
| 方法、分组依赖 | 编排必须按阶段执行的端到端流程 |
| 并行执行 | 按 suite、test、class、method 或实例并发运行 |
| Listener、Reporter | 监听执行过程并生成自定义报告 |
截至 2026 年 7 月 21 日,TestNG 最新稳定版为 7.12.0,发布于 2026 年 1 月 22 日。该版本发布产物面向 Java 11 字节码,运行时要求 Java 11+;从源码构建 TestNG 则需要 JDK 21+,两者不要混淆。
使用 Maven 集成 TestNG
示例目录如下:
text
testng-demo/
├── pom.xml
└── src/
├── main/java/demo/
│ └── OrderService.java
└── test/
├── java/demo/
│ ├── OrderServiceTest.java
│ └── OrderStatusTest.java
└── resources/
└── testng.xml
pom.xml:
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>demo</groupId>
<artifactId>testng-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.release>11</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<testng.version>7.12.0</testng.version>
<maven.compiler.version>3.13.0</maven.compiler.version>
</properties>
<dependencies>
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>${testng.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven.compiler.version}</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.5</version>
</plugin>
</plugins>
</build>
</project>
Surefire 检测到 TestNG 依赖后会自动使用 TestNG Provider。项目中应固定 TestNG、编译插件和 Surefire 版本,并通过 mvn -version 确认本地与 CI 使用的 JDK 一致。
TestNG 依赖 slf4j-api,但不强制选择日志实现。项目没有 SLF4J Provider 时会出现提示,不影响测试结果;需要 TestNG 日志时,应复用项目现有 Provider,避免为了消除提示重复引入日志实现。
准备被测代码
示例业务规则:
- 只有待支付订单可以取消
- 取消成功后订单状态变为已取消
- 取消订单需要释放对应数量的库存
- 已支付或不存在的订单取消失败,且不能释放库存
- 已取消订单再次取消失败,且不能重复释放库存
创建 src/main/java/demo/OrderService.java:
java
package demo;
import java.util.HashMap;
import java.util.Map;
enum OrderStatus {
PENDING,
PAID,
CANCELLED;
boolean canCancel() {
return this == PENDING;
}
}
final class Order {
private final long id;
private final int quantity;
private final OrderStatus status;
Order(long id, int quantity, OrderStatus status) {
this.id = id;
this.quantity = quantity;
this.status = status;
}
long getId() {
return id;
}
int getQuantity() {
return quantity;
}
OrderStatus getStatus() {
return status;
}
Order withStatus(OrderStatus newStatus) {
return new Order(id, quantity, newStatus);
}
}
final class OrderService {
private final Map<Long, Order> orders = new HashMap<>();
private int releasedQuantity;
void add(Order order) {
orders.put(order.getId(), order);
}
Order cancel(long orderId) {
Order order = orders.get(orderId);
if (order == null) {
throw new IllegalArgumentException("订单不存在");
}
if (!order.getStatus().canCancel()) {
throw new IllegalStateException("当前状态不允许取消");
}
releasedQuantity += order.getQuantity();
Order cancelled = order.withStatus(OrderStatus.CANCELLED);
orders.put(orderId, cancelled);
return cancelled;
}
int getReleasedQuantity() {
return releasedQuantity;
}
}
这里使用内存实现,是为了把重点放在 TestNG 的测试组织与调度。为控制示例篇幅,三个类型都只在 demo 包内使用;真实项目应拆成独立文件,并按业务调用范围设置可见性。OrderService 包含可变状态,不适合被多个并行测试共享;数据库和远程依赖应在单元测试中隔离,在集成测试中使用独立数据。
生命周期与断言
创建 src/test/java/demo/OrderServiceTest.java:
java
package demo;
import org.testng.Assert;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
public class OrderServiceTest {
private OrderService service;
@BeforeMethod(alwaysRun = true)
public void setUp() {
service = new OrderService();
}
@Test(groups = {"smoke", "regression"})
public void cancelsPendingOrderAndReleasesInventory() {
service.add(new Order(1001L, 2, OrderStatus.PENDING));
Order actual = service.cancel(1001L);
Assert.assertEquals(actual.getStatus(), OrderStatus.CANCELLED);
Assert.assertEquals(actual.getId(), 1001L);
Assert.assertEquals(service.getReleasedQuantity(), 2);
}
@Test(groups = "regression")
public void rejectsPaidOrderWithoutReleasingInventory() {
service.add(new Order(1002L, 1, OrderStatus.PAID));
IllegalStateException error = Assert.expectThrows(
IllegalStateException.class,
() -> service.cancel(1002L)
);
Assert.assertEquals(error.getMessage(), "当前状态不允许取消");
Assert.assertEquals(service.getReleasedQuantity(), 0);
}
@Test(groups = "regression")
public void doesNotReleaseInventoryTwiceWhenCancelledAgain() {
service.add(new Order(1003L, 2, OrderStatus.PENDING));
service.cancel(1003L);
IllegalStateException error = Assert.expectThrows(
IllegalStateException.class,
() -> service.cancel(1003L)
);
Assert.assertEquals(error.getMessage(), "当前状态不允许取消");
Assert.assertEquals(service.getReleasedQuantity(), 2);
}
@Test(groups = "regression")
public void rejectsMissingOrderWithoutReleasingInventory() {
IllegalArgumentException error = Assert.expectThrows(
IllegalArgumentException.class,
() -> service.cancel(9999L)
);
Assert.assertEquals(error.getMessage(), "订单不存在");
Assert.assertEquals(service.getReleasedQuantity(), 0);
}
}
@BeforeMethod 会在每个测试方法前执行,但 TestNG 默认不会为每个方法重新创建测试类实例,同一类的方法会共享实例字段。方法级并行时,这些字段可能被多个线程同时访问。示例中的 OrderServiceTest 不使用并行 DataProvider,再配合 parallel="classes",同一类的普通测试方法会在同一线程执行。
TestNG 的 Assert.assertEquals() 参数顺序是"实际值在前、期望值在后",与 JUnit 常见写法相反。Assert.expectThrows() 会返回捕获到的异常,并接受指定异常的子类;必须严格匹配类型时,再检查 error.getClass()。
需要一次检查多个字段时,可以使用 SoftAssert,但最后必须调用 assertAll(),否则失败不会抛出:
java
SoftAssert softly = new SoftAssert();
softly.assertEquals(actual.getStatus(), OrderStatus.CANCELLED);
softly.assertEquals(actual.getQuantity(), 2);
softly.assertAll();
SoftAssert 应在当前测试方法内创建,不要作为多个测试共享的实例字段,否则断言结果可能相互污染,并行执行时还会产生线程安全问题。
DataProvider:批量验证状态规则
创建 src/test/java/demo/OrderStatusTest.java:
java
package demo;
import org.testng.Assert;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
public class OrderStatusTest {
@DataProvider(name = "statusCases", parallel = true)
public Object[][] statusCases() {
return new Object[][] {
{OrderStatus.PENDING, true},
{OrderStatus.PAID, false},
{OrderStatus.CANCELLED, false}
};
}
@Test(dataProvider = "statusCases", groups = "regression")
public void onlyPendingStatusCanBeCancelled(
OrderStatus status,
boolean expected
) {
Assert.assertEquals(status.canCancel(), expected);
}
}
@DataProvider 返回的每一行对应一次测试调用。这里开启并行是安全的,因为测试只调用无状态的枚举方法;如果数据驱动测试共用账号、订单或类字段,就必须为每次调用准备独立数据,不能只加一个 parallel = true。
复杂对象可以直接放入 Object[][],数据来自文件或数据库时也可以返回 Iterator<Object[]>。测试数据应直接写出期望值,不要在 DataProvider 中复制生产判断逻辑。
分组与 testng.xml
当前示例使用两个分组:
smoke:验证待支付订单能否正常取消regression:覆盖全部状态和异常路径
需要固定测试类、分组和并行策略时,创建 src/test/resources/testng.xml:
xml
<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.1.dtd">
<suite name="order-suite"
parallel="classes"
thread-count="2"
data-provider-thread-count="3">
<groups>
<run>
<include name="smoke"/>
<include name="regression"/>
</run>
</groups>
<test name="order-tests">
<classes>
<class name="demo.OrderServiceTest"/>
<class name="demo.OrderStatusTest"/>
</classes>
</test>
</suite>
parallel="classes" 表示不同测试类可以并行,同一测试类中的普通测试方法仍在同一线程执行。常见并行粒度如下:
| 配置 | 执行方式 |
|---|---|
methods |
测试方法并行,最容易遇到共享字段竞争 |
classes |
测试类并行,同一类的普通测试方法使用同一线程 |
tests |
不同 <test> 并行,同一 <test> 内方法使用同一线程 |
instances |
不同测试类实例并行 |
@DataProvider(parallel = true) 是例外:同一测试方法的多组参数会进入独立的 DataProvider 线程池,不受"同类普通方法使用同一线程"的保护。thread-count 控制常规并行线程数,data-provider-thread-count 控制并行 DataProvider 的线程池。TestNG 通过 XML 执行并行 DataProvider 时默认使用 10 个线程,本例主动限制为 3。线程越多不代表越快,数据库连接池、测试账号、限流和测试数据通常先成为瓶颈。
依赖测试:只用于真正的流程依赖
TestNG 可以声明方法或分组依赖:
java
@Test(groups = "order-flow")
public void createsOrder() {
// 创建订单
}
@Test(
groups = "order-flow",
dependsOnMethods = "createsOrder"
)
public void paysOrder() {
// 支付订单
}
createsOrder() 失败后,paysOrder() 会标记为 SKIP,而不是 FAIL。这适合一个不可拆分的端到端流程,不适合普通单元测试;大量使用依赖会形成跳过链,真正的失败容易被淹没。公共前置条件应放在 @BeforeMethod、@BeforeClass 等配置方法中,清理方法使用 alwaysRun = true。
执行与报告
bash
# 自动发现并执行全部 *Test 测试类
mvn test
# 只执行冒烟分组
mvn -Dgroups=smoke test
# 按 testng.xml 执行
mvn -Dsurefire.suiteXmlFiles=src/test/resources/testng.xml test
本文示例在 Java 17.0.19、Maven 3.9.11 环境中实际执行结果:
| 执行方式 | 测试数 | 结果 |
|---|---|---|
| 全量执行 | 7 | 全部通过 |
smoke 分组 |
1 | 通过 |
| XML Suite | 7 | 全部通过 |
Surefire 报告保存在 target/surefire-reports:CI 通常归档 TEST-*.xml,排查本次执行可查看 testng-results.xml 和 index.html。CI 命令建议使用批处理模式:
bash
mvn -B -Dsurefire.suiteXmlFiles=src/test/resources/testng.xml test
需要实时监听成功、失败和跳过事件时实现 ITestListener;需要在全部 Suite 完成后汇总报告时实现 IReporter。监听器中不要吞掉异常,也不要把报告上传失败反向修改业务测试结果。
从 JUnit 转到 TestNG 时注意什么
| JUnit Jupiter | TestNG |
|---|---|
@BeforeEach、@AfterEach |
@BeforeMethod、@AfterMethod |
@ParameterizedTest + 参数源 |
@Test(dataProvider = "...") + @DataProvider |
@Tag("smoke") |
@Test(groups = "smoke") |
assertEquals(expected, actual) |
Assert.assertEquals(actual, expected) |
| 默认每个方法使用新测试实例 | 默认同一测试类的方法共享实例 |
迁移不能只替换注解,测试实例生命周期和断言参数顺序也要一起检查。另外,TestNG 7.12.0 要求 Java 11+,仍运行在 Java 8 的项目不能直接升级到该版本。
常见问题
| 问题 | 处理方式 |
|---|---|
| 测试没有执行 | 检查测试类命名、Surefire 版本和 org.testng.annotations.Test 导入 |
| DataProvider 参数不匹配 | 检查每行参数数量、顺序和测试方法类型 |
| SoftAssert 没有报错 | 确认测试结束前调用了 assertAll() |
| 大量测试显示 SKIP | 检查 dependsOnMethods、dependsOnGroups 的上游失败 |
| 并行时偶发失败 | 排查共享字段、静态变量、账号、文件和测试数据冲突 |
| 分组执行结果不完整 | 检查类级与方法级 groups,以及 XML 中的 include、exclude |
| 清理逻辑没有执行 | 为 @AfterMethod、@AfterClass 设置 alwaysRun = true |
工程实践建议
- 使用
smoke、regression等稳定分组,不把需求编号当作长期分组名 - 每个测试独立准备和清理数据,避免依赖执行顺序
- DataProvider 只提供数据,不在其中执行核心业务逻辑
- 开启并行前先证明测试线程安全,再逐步增加线程数
- 依赖测试只用于不可拆分的端到端流程
- 单元测试、接口测试和 UI 测试分开配置 Suite 与并发策略
- CI 保留 Surefire XML、失败日志和 TestNG 报告
总结
TestNG 的核心不只是 @Test,而是测试集的组织与调度:用分组控制执行范围,用 DataProvider 覆盖规则表,用 testng.xml 固化 Suite,再在数据隔离后开启并行。
依赖和并行都是双刃剑。测试能够独立执行、失败后容易定位,再谈编排和提速,才能避免自动化测试集变成不稳定的执行链。