接口测试框架重构:YAML 用例驱动 + 分层设计

将接口测试按职责拆分为数据、驱动、执行、断言、API 封装等独立板块,让项目结构更清晰,每个文件只负责单一职责,避免逻辑杂糅。

  • 数据层 :brand.yaml 等用例数据,与代码分离;
  • 数据提供层 :YamlDataProvider 读取 YAML,转成 TestNG 可用的数据;
  • 执行层 :AbstractCaseTest 统一"解析 → 调用 → 断言 → 提取"流程,子类只声明差异;
  • API 封装层 :BrandApi 等封装具体接口请求;
  • 分发层 :BrandApiDispatcher 按 method 把用例分发到对应接口;
  • 断言层 :ResponseAsserter 负责响应校验;
  • 变量层 :VariableResolver 处理占位符替换,TestContext 承载跨用例变量。

一、概括如下:

纯结构关系图

关系说明:

关系 说明
ApiBase → AbstractCaseTest → BrandBase → BrandTest 继承链,extends 一路往下
BrandBase → 构造 BrandApi / BrandApiDispatcher BrandBase.init() 里 new
BrandApiDispatcher → 持有 BrandApi 构造器注入
YamlDataProvider → 读取 brand.yaml YamlLoader.load(...)
YamlDataProvider → 给 BrandTest 提供数据 @DataProvider(name="brandCases")
BrandTest → new 出 AbstractCaseTest 的 context 基类字段初始化

二、详细分析和代码如下:

1. ApiBase - 全局 RestAssured 配置,通过@BeforeSuite注解让整个测试套件只执行一次

java 复制代码
public class ApiBase {

    @BeforeSuite(alwaysRun = true)
    public void globalSetup() {
        String baseUrl = ConfigReader.getProperty("api.base.url");

        // 通用请求规范:baseUri、Content-Type、Accept
        RequestSpecification requestSpec = new RequestSpecBuilder()
                .setBaseUri(baseUrl)
                .addHeader("Content-Type", "application/json")
                .addHeader("Accept", "application/json")
                .build();

        // 通用响应规范:响应时间上限
        ResponseSpecification responseSpec = new ResponseSpecBuilder()
                .expectResponseTime(Matchers.lessThan(20000L))
                .build();

        RestAssured.requestSpecification = requestSpec;
        RestAssured.responseSpecification = responseSpec;

        // 可选:全局日志、编码
        RestAssured.enableLoggingOfRequestAndResponseIfValidationFails();
    }
}

2. AbstractCaseTest

  • 规定通用执行流程:取出 case 的各个字段 --> 做变量替换 --> 根据 method 分发调用不同接口 --> 断言 --> 提取变量供后续case使用
  • 继承ApiBase:在这个类中继承ApiBase是因为Java中每个类只能继承一个类,xxBase之前是要继承ApiBase的,现在把通用执行流程抽离出来,xxBase必须继承AbstractCaseTest,所以让AbstractCaseTest继承ApiBase。
java 复制代码
public abstract class AbstractCaseTest extends ApiBase {

    /** 子类提供自己的 dispatcher */
    protected abstract ApiDispatcher dispatcher();

    /** 每个子类共享的运行时上下文 */
    protected final TestContext context = new TestContext();

    /** 通用执行流程 */
    protected void execute(Map<String, Object> caseData) {
        String caseName = (String) caseData.get("case");
        String method   = (String) caseData.get("method");

        @SuppressWarnings("unchecked")
        Map<String, Object> data = (Map<String, Object>) caseData.get("data");

        @SuppressWarnings("unchecked")
        Map<String, Object> expected = (Map<String, Object>) caseData.get("expected");

        System.out.println("====== " + caseName + " ======");

        // 1. 变量替换
        Map<String, Object> resolvedData = VariableResolver.resolve(data, context);
        Map<String, Object> resolvedExpected = VariableResolver.resolve(expected, context);

        // 2. 分发调用
        Response resp = dispatcher().dispatch(method, resolvedData);
        resp.then().log().all();

        // 3. 断言
        ResponseAsserter.assertResponse(resp, resolvedExpected);

        // 4. 提取变量供后续 case 使用
        extractVariables(caseData, resolvedData, resp);
    }

    /** 提取变量到 context */
    @SuppressWarnings("unchecked")
    protected void extractVariables(Map<String, Object> caseData,
                                    Map<String, Object> resolvedData,
                                    Response resp) {
        Map<String, String> extract = (Map<String, String>) caseData.get("extract");
        if (extract == null) return;

        for (Map.Entry<String, String> e : extract.entrySet()) {
            String path = e.getValue();
            Object v = path.startsWith("req.")
                    ? getByPath(resolvedData, path.substring(4))  // 从请求取
                    : resp.jsonPath().get(path);                  // 从响应取
            context.put(e.getKey(), v);
        }
    }

    /** 按 a.b.c 路径从嵌套 Map 取值 */
    @SuppressWarnings("unchecked")
    protected static Object getByPath(Map<String, Object> data, String path) {
        if (data == null || path == null || path.isEmpty()) {
            return null;
        }
        Object current = data;
        for (String part : path.split("\\.")) {
            if (!(current instanceof Map)) {
                return null;
            }
            current = ((Map<String, Object>) current).get(part);
            if (current == null) {
                return null;
            }
        }
        return current;
    }
}

3. BrandBase - 通过@BeforeClass注解在当前测试类的第一个测试方法前执行一次,负责登录拿token + 构造BrandApi/Dispatcher

java 复制代码
public abstract class BrandBase extends AbstractCaseTest {

    protected BrandApi brandApi;
    protected BrandApiDispatcher brandApiDispatcher;

    @BeforeClass
    protected void init() {
        String token = AuthApi.login();

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

        brandApi = new BrandApi(authSpec);
        brandApiDispatcher = new BrandApiDispatcher(brandApi);
    }

}

4. BrandApi

  • 包含品牌模块所有接口定义
  • 约定:所有方法签名统一为 Map<String,Object> -> Response,这样 yaml 里的 method 才能通过 Dispatcher 统一调用。
java 复制代码
public class BrandApi {

    private final RequestSpecification authSpec;

    public BrandApi(RequestSpecification authSpec) {
        this.authSpec = authSpec;
    }

    /** 创建品牌 */
    public Response create(Map<String, Object> data) {
        return given().spec(authSpec)
                .body(data)
                .post("/create");
    }
    // 其他请求

5. BrandTest

  • 实现 AbstractCaseTest 声明的抽象方法dispatcher
  • 接收已经加载好的一条 case(一个 Map),是解析这条 case 的字段并执行
java 复制代码
public class BrandTest extends BrandBase {

    // 父类 BrandBase 里已有 brandApiDispatcher 字段
    @Override
    protected ApiDispatcher dispatcher() {
        return brandApiDispatcher;
    }

    @Test(dataProvider = "brandCases", dataProviderClass = YamlDataProvider.class)
    public void test_brand(Map<String, Object> caseData) {
        execute(caseData);
    }
}

6. BrandApiDispatcher - 把 yaml 里的 method 字符串,映射到 BrandApi 的具体方法

java 复制代码
public class BrandApiDispatcher implements ApiDispatcher {

    private final BrandApi api;

    public BrandApiDispatcher(BrandApi api) {
        this.api = api;
    }

    public Response dispatch(String method, Map<String, Object> data) {
        switch (method) {
            case "create":              return api.create(data);
            default:
                throw new IllegalArgumentException("未知的 method: " + method);
        }
    }
}

7. YamlDataProvider

  • 读 data/brand.yaml,每个 case 一行。
  • yaml 顶层是 list,YamlLoader 返回 Object\[\]\[\]。
java 复制代码
public class YamlDataProvider {

    /**
     * 读 data/brand.yaml,每个 case 一行。
     * yaml 顶层是 list,YamlLoader 返回 Object[][]。
     */
    @DataProvider(name = "brandCases")
    public static Object[][] brandCases() {
        return YamlLoader.load("data/brand.yaml");
    }
}

8. brand.yaml文件

  • method 对应 BrandApiDispatcher 里的 case
  • data 里的 ${xxx} 会被 VariableResolver 替换
  • expected 的 key 是 JsonPath,value 是期望值
  • extract 把响应字段存进 context,供后续 case 用
yaml 复制代码
- case: 创建品牌
  method: create
  data:
    name: "海尔_${timestamp}"
    firstLetter: "H"
    sort: 5
    factoryStatus: 0
    showStatus: 0
    logo: "https://cdn.example.com/brand/haier-logo.png"
    bigPic: "https://cdn.example.com/brand/haier-big.jpg"
    brandStory: "海尔创立于 1984 年,是全球领先的美好生活解决方案服务商。"
  expected:
    code: 200
    message: "操作成功"
    data: 1
  extract:
    name: "req.name"
相关推荐
用户5446933925644 分钟前
为什么有管理员权限,Token正确,访问却是403
后端
nyaomaru44 分钟前
你的 Type Guard 可能会悄悄地与 TypeScript 类型发生偏移 🔧
后端·typescript
明月_清风44 分钟前
数据平台到底是什么?一篇文章搞懂数据平台开发
大数据·后端·数据分析
心之语歌44 分钟前
DeerFlow Docker Desktop 部署教程
后端
美好世界44 分钟前
Codex 源码导读:第二部分——一次 Turn 的完整生命周期
后端
海岳云舟44 分钟前
SpringCloudGateway 动态转发后端服务
后端
我的div丢了肿么办44 分钟前
自定义类型和类型别名以及实例化结构体的5种方式
后端·go
小满zs1 小时前
Go语言第十三章(互斥锁,读写锁)
后端·go
IT_陈寒1 小时前
JavaScript的this指向问题又让我加了个班
前端·人工智能·后端