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,否则可能出现报错。

相关推荐
猿究院_夏燊2 小时前
Spring Bean 生命周期
java·后端·spring
Jlzn88883 小时前
华东电芯分选通道数怎么配才不堵线
java·人工智能·架构·自动化·制造
CCblandy4 小时前
雪花 ID 四个边界实测:时钟回拨重号 500 个,前端 JSON.parse 直接丢精度
后端·python·算法·性能优化·架构
我是人✓4 小时前
SpringBoot项目的五种创建方式
java·spring boot·后端
阡陌数智4 小时前
Gemini 4 Argon 百万 Token 输出能力解析:原生长输出模型的工程落地与并发成本治理
人工智能·架构
念念清晰5 小时前
000-OpenJDK17 源码构建:MacOS环境(M芯片)
java·后端
来福猿6 小时前
SpringCloud Config 源码分析:配置拉取、刷新与多环境管理
后端·spring·spring cloud
海宇数据7 小时前
零信任架构实战:基于海宇入职背调报告构建自动化即时配送与网约车众包运力准入鉴权网关
运维·人工智能·架构·自动化
IT_陈寒8 小时前
React组件莫名重渲染?你可能忘了这个小细节
前端·人工智能·后端
天远API8 小时前
零信任架构实战:基于天远公安二要素政务版构建自动化不动产交易与契税核算实名核实网关
人工智能·架构·自动化·php