接口测试框架重构:YAML 用例驱动与分层设计
英文标题:接口测试框架重构:YAML 用例驱动 + 分层设计
该方案将接口测试按数据、数据提供、执行、API 封装、分发、断言和变量管理等职责拆分,实现 YAML 用例与测试代码分离。`AbstractCaseTest` 统一承担“解析 → 调用 → 断言 → 提取”执行链路,业务测试类只需声明对应的 Dispatcher。通过 `YamlDataProvider` 向 TestNG 提供用例数据,并借助 `TestContext` 和 `VariableResolver` 支持跨用例变量传递。该分层设计有助于降低接口测试项目的耦合度、提升新增接口和维护用例时的效率。
64.9 KB
6.3 KB
中文
将接口测试按职责拆分为数据、驱动、执行、断言、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 → 创建 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
- 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"
English / 原文
将接口测试按职责拆分为数据、驱动、执行、断言、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注解让整个测试套件只执行一次
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 用
- 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"