[测试技术] TestNG 入门与实战:分组、数据驱动与并行执行

原创内容,未获授权禁止转载、转发、抄袭。

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.xmlindex.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 检查 dependsOnMethodsdependsOnGroups 的上游失败
并行时偶发失败 排查共享字段、静态变量、账号、文件和测试数据冲突
分组执行结果不完整 检查类级与方法级 groups,以及 XML 中的 include、exclude
清理逻辑没有执行 @AfterMethod@AfterClass 设置 alwaysRun = true

工程实践建议

  • 使用 smokeregression 等稳定分组,不把需求编号当作长期分组名
  • 每个测试独立准备和清理数据,避免依赖执行顺序
  • DataProvider 只提供数据,不在其中执行核心业务逻辑
  • 开启并行前先证明测试线程安全,再逐步增加线程数
  • 依赖测试只用于不可拆分的端到端流程
  • 单元测试、接口测试和 UI 测试分开配置 Suite 与并发策略
  • CI 保留 Surefire XML、失败日志和 TestNG 报告

总结

TestNG 的核心不只是 @Test,而是测试集的组织与调度:用分组控制执行范围,用 DataProvider 覆盖规则表,用 testng.xml 固化 Suite,再在数据隔离后开启并行。

依赖和并行都是双刃剑。测试能够独立执行、失败后容易定位,再谈编排和提速,才能避免自动化测试集变成不稳定的执行链。

相关推荐
START_GAME3 小时前
LuaTools的使用指南
junit
川石课堂软件测试21 小时前
安全测试|常见SQL注入攻击方式、实例及预防
服务器·数据库·sql·功能测试·测试工具·安全·单元测试
测试修炼手册1 天前
[测试技术] JUnit 6 入门与实战:断言、参数化与 Mockito
数据库·junit·sqlserver
在水一缸1 天前
深入浅出 Catch2:现代 C++ 测试框架的优雅实践
开发语言·c++·单元测试·log4j·测试框架·catch2
花椒技术2 天前
AI Coding 后半程:6 类 QA Skills 如何接住测试与发布?
单元测试·ai编程·测试
疯狂打码的少年2 天前
【软件工程】软件测试阶段:单元测试与集成测试(高频对比考点)
笔记·单元测试·集成测试·软件工程
●VON2 天前
鸿蒙 PC Markdown 编辑器查找系统:大小写、整词与循环定位
华为·单元测试·编辑器·harmonyos·鸿蒙
川石课堂软件测试3 天前
安全测试|服务器安全加固方法
服务器·功能测试·测试工具·jmeter·mysql·web安全·单元测试
测试修炼手册4 天前
[测试技术] GoogleTest 入门与实战:单元测试、参数化与 Mock
单元测试