中文资讯 掘金 AI Sat, 26 Sep 2026 正文 ok 680 words

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

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

该方案将接口测试按数据、数据提供、执行、API 封装、分发、断言和变量管理等职责拆分,实现 YAML 用例与测试代码分离。`AbstractCaseTest` 统一承担“解析 → 调用 → 断言 → 提取”执行链路,业务测试类只需声明对应的 Dispatcher。通过 `YamlDataProvider` 向 TestNG 提供用例数据,并借助 `TestContext` 和 `VariableResolver` 支持跨用例变量传递。该分层设计有助于降低接口测试项目的耦合度、提升新增接口和维护用例时的效率。
打开原文相关 2 · 机会 2
media3.2 KB media589 B media64.9 KB media6.3 KB

中文

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

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

一、概括如下:

纯结构关系图

deepseek_mermaid_20260926_062bac.png

关系说明:

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

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

1. ApiBase:全局 RestAssured 配置

通过 @BeforeSuite 注解使其在整个测试套件中仅执行一次。

java public class ApiBase {

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

    
    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:由于 Java 中每个类只能继承一个类,xxBase 原本需要继承 ApiBase。现在将通用执行流程抽离后,xxBase 必须继承 AbstractCaseTest,因此让 AbstractCaseTest 继承 ApiBase。

java public abstract class AbstractCaseTest extends ApiBase {

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 + " ======");

    
    Map<String, Object> resolvedData = VariableResolver.resolve(data, context);
    Map<String, Object> resolvedExpected = VariableResolver.resolve(expected, context);

    
    Response resp = dispatcher().dispatch(method, resolvedData);
    resp.then().log().all();

    
    ResponseAsserter.assertResponse(resp, resolvedExpected);

    
    extractVariables(caseData, resolvedData, resp);
}


@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);
    }
}


@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:初始化认证与 API 对象

通过 @BeforeClass 注解,在当前测试类的第一个测试方法执行前运行一次,负责登录获取 token,并构造 BrandApi 和 BrandApiDispatcher。

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 {

@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 {

@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

English / 原文

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

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

一、概括如下:

纯结构关系图

deepseek_mermaid_20260926_062bac.png

关系说明:

关系 说明
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注解让整个测试套件只执行一次

public class ApiBase {

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

        
        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。

    public abstract class AbstractCaseTest extends ApiBase {

    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 + " ======");
    
        
        Map<String, Object> resolvedData = VariableResolver.resolve(data, context);
        Map<String, Object> resolvedExpected = VariableResolver.resolve(expected, context);
    
        
        Response resp = dispatcher().dispatch(method, resolvedData);
        resp.then().log().all();
    
        
        ResponseAsserter.assertResponse(resp, resolvedExpected);
    
        
        extractVariables(caseData, resolvedData, resp);
    }
    
    
    @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);
        }
    }
    
    
    @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

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 统一调用。

    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 的字段并执行

    public class BrandTest extends BrandBase {

    @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 的具体方法

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[][]。

    public class YamlDataProvider {

    @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 用