TestNG 分组接入现有框架:实现、踩坑与解法

在现有框架基础上,加上groups字段对case进行分组,例如分成smoke, regression......

适用于只需要对部分case进行测试,而不需要run yaml中所有case的情况。

现有框架: 接口测试框架重构:YAML 用例驱动 + 分层设计将接口测试按职责拆分为数据、驱动、执行、断言、API 封装等独立板块, - 掘金

首先目前用的框架AdminApi只是封装了HTTP请求方法,它本身不是测试类,所以@Test不应该写在这个文件中。

其次AdminTest只有一个统一的 test_admin(Map caseData) 方法,所有用例都通过 YAML 数据进来 → 如果在这个方法上写 @Test(groups = {"smoke"}),那所有 YAML 里的用例都会变成 smoke,失去分组的意义。

所以最合适的做法是:把 group 作为 YAML 用例的一个字段

具体实现如下:

1. YAML 用例文件(新增 groups 字段)

yaml 复制代码
- case: 注册管理员
  method: register
  groups: ["smoke"]
  data:
    username: "${username}"
    password: "123456"
    icon: "github_icon.png"
    email: "${email}"
    nickName: "autotest"
    note: "auto register admin user"
  expected:
    code: 200
    message: "操作成功"
    data.username: "${username}"
    data.icon: "github_icon.png"
    data.email: "${email}"
    data.nickName: "autotest"
    data.note: "auto register admin user"
    data.status: 1
  extract:
    registeredAdminId: "data.id"

- case: 管理员登录
  method: login
  groups: ["smoke", "regression"]
  data:
    username: "${system.admin.username}"
    password: "${system.admin.password}"
  expected:
    code: 200
    message: "操作成功"
  extract:
    adminToken: "data.token"

- case: 获取用户信息
  method: getInfo
  groups: ["regression"]
  data:
  expected:
    code: 200
    message: "操作成功"
    data.username: "${system.admin.username}"

2. YamlDataProvider(改动核心:按 group 过滤)

java 复制代码
public class YamlDataProvider {

    /**
     * 读 data/xx.yaml,每个 case 一行。
     * yaml 顶层是 list,YamlLoader 返回 Object[][]。
     */
    private static final String CASE_FILE = "/data/admin.yaml";

    /** 加载 YAML 全部用例 */
    @SuppressWarnings("unchecked")
    private static List<Map<String, Object>> loadAllCases() {
        try (InputStream in = YamlDataProvider.class.getResourceAsStream(CASE_FILE)) {
            if (in == null) {
                throw new IllegalStateException("找不到用例文件: " + CASE_FILE);
            }
            return new Yaml().load(in);
        } catch (Exception e) {
            throw new RuntimeException("加载 YAML 用例失败", e);
        }
    }

    /** 按 group 过滤,返回 Object[][] */
    @SuppressWarnings("unchecked")
    private static Object[][] filterByGroup(String group) {
        return loadAllCases().stream()
                .filter(c -> {
                    Object groups = c.get("groups");
                    if (groups instanceof List) {
                        return ((List<String>) groups).contains(group);
                    }
                    return false;
                })
                .map(c -> new Object[]{c})
                .toArray(Object[][]::new);
    }

    /** smoke 组用例 */
    @DataProvider(name = "smokeCases")
    public static Object[][] smokeCases() {
        return filterByGroup("smoke");
    }

    /** regression 组用例 */
    @DataProvider(name = "regressionCases")
    public static Object[][] regressionCases() {
        return filterByGroup("regression");
    }
}

3. AdminTest(拆成 smoke / regression 两个入口)

java 复制代码
public class AdminTest extends AdminBase {
    @Override
    protected ApiDispatcher dispatcher() {
        return adminApiDispatcher;
    }

    @BeforeMethod(alwaysRun = true)
    public void prepareRuntimeData() {
        System.out.println(">>> prepareRuntimeData 执行, instance=" + this.hashCode());
        context().clear();   // 每个用例重置,避免串数据
        String ts = String.valueOf(System.currentTimeMillis());
        context().put("username", "admin_" + ts);
        context().put("email", "admin_" + ts + "@test.com");
        System.out.println(">>> context after prepare: " + context());
    }

    /** 冒烟:只跑 YAML 里 groups 含 smoke 的用例 */
    @Test(dataProvider = "smokeCases",
            dataProviderClass = YamlDataProvider.class,
            groups = {"smoke"})
    public void test_admin_smoke(Map<String, Object> caseData) {
        execute(caseData);
    }

    /** 回归:只跑 YAML 里 groups 含 regression 的用例 */
    @Test(dataProvider = "regressionCases",
            dataProviderClass = YamlDataProvider.class,
            groups = {"regression"})
    public void test_admin_regression(Map<String, Object> caseData) {
        execute(caseData);
    }
}

4. testng.xml中加上groups

xml 复制代码
    <test name="admin-smoke">
        <groups>
            <run>
                <include name="smoke"/>
            </run>
        </groups>
        <classes>
            <class name="test.admin.AdminTest"/>
        </classes>
    </test>

以上这些可以实现分组跑case了,不过在跑的过程中碰到了两个bug.

  1. 变量未定义: email,这个来源是smoke groups的第一个case,注册功能。正常逻辑执行是在register方法执行前从 AdminTest的prepareRuntimeData方法中获取email变量的值(@BeforeMethod注解),但是打印日志发现根本没执行prepareRuntimeData()。没执行的原因是testng.xml中配了只跑groups = "smoke"的方法/case,TestNG 的 group 过滤机制对 @BeforeMethod 也生效 ,而prepareRuntimeData() 是 @BeforeMethod,它没有指定 groups ,所以不执行。最后的解决方案是给 @BeforeMethod 加 alwaysRun = true。
java 复制代码
@BeforeMethod
public void prepareRuntimeData() {
    String ts = String.valueOf(System.currentTimeMillis());
    context().put("email", "admin_" + ts + "@test.com");
}
  1. dispatcher() is null,在调用AbstractCaseTest中execute方法时用到了dispatcher() 出现了这个报错。
java 复制代码
public abstract class AbstractCaseTest extends ApiBase {

    /** 通用执行流程 */
    protected void execute(Map<String, Object> caseData) {
        // 2. 分发调用
        Response resp = dispatcher().dispatch(method, resolvedData);
        resp.then().log().all();

实际上正常流程在AdminBase方法初始化时构造了Dispatcher,然后可以在AbstractCaseTest中调用dispatcher(),打印日志发现AdminBase中的init()方法并未被执行。原因和第一个问题一样,TestNG 的 group 过滤机制对 @BeforeClass 也生效 ,而init() 是 @BeforeClass,它没有指定 groups ,所以不执行。最后的解决方案也是给 @BeforeClass 加 alwaysRun = true。

java 复制代码
public abstract class AdminBase extends AbstractCaseTest {
    protected AdminApi adminApi;
    protected AdminApiDispatcher adminApiDispatcher;
    
    @BeforeClass
    protected void init() {
        String token = AuthApi.login();

        RequestSpecification authSpec = given()
                .basePath("/admin")
                .header("Authorization", "Bearer " + token);

        RequestSpecification noAuthSpec = given()
                .basePath("/admin");

        adminApi = new AdminApi(authSpec, noAuthSpec);
        adminApiDispatcher = new AdminApiDispatcher(adminApi);
    }

}

总结:如果在testng中限定了groups,一定要注意其他加了注解的方法是否必须要执行,对于必须执行的方法可以加上 alwaysRun = true,否则可能出现报错。

相关推荐
独泪了无痕1 小时前
Hutool之RandomUtil:随机数生成的终极利器
java·后端
imDwAaY1 小时前
6篇文章讲清楚Git:Git 远程协作:fetch、pull、push 到底有什么区别?(3/6)
git·后端
金銀銅鐵1 小时前
[Java] 通过行为参数化传递代码
后端·函数式编程
浪遏1 小时前
D2C 系统架构复盘:从设计稿到代码,我们踩过的坑和最终方案
前端·javascript·后端
美好世界1 小时前
Codex 源码导读:第四部分——上下文压缩与继续执行
架构
美好世界1 小时前
Codex 源码导读:第十部分——Session、Thread 持久化与 Memory
架构
明月_清风1 小时前
数据处理完放在哪里?一文搞懂数据仓库与数据湖
大数据·后端·数据分析
IT_陈寒1 小时前
我的JavaScript代码为啥在forEach里没按预期执行?
前端·人工智能·后端
farerboy1 小时前
WEB 项目如何禁用 F12 等功能
前端·vue.js·架构