接口自动化测试的一些思考和我自己的一整套处理方案 (java 向)

接口自动化测试的一些思考

前言

之前社区一直讨论很火的接口测试框架实现,到底是高大上的傻瓜式接口平台好用,还是全脚本的编写的接口框架好这两个方案其实我都有考虑过,两个方案各有优缺点, 我个人理解就是接口平台优点可以降低学习成本,快速使用, 全脚本编写接口框架的优点为灵活性高,各种疑难杂症的用例都能解决,而且拓展性好

在思考这两条路上,我差点做了接口平台,主要基于两点,第一点就是我上家公司,在接口平台还没火起来还没这个概念的时候,我写的接口测试框架基本上是全代码实现,在后面的大面积应用过程中,我也感觉到了很多缺点,比如分层不清晰, 大量用例维护心累等问题.第二点是接口平台能在像领导汇报时候提升逼格,出去面试时候也能吹吹

最后的结果就是何不两种方案结合下,可以提供一个测试数据模板直接把用例编写好,然后代码端又提供很好的拓展,有了这个想法就开始了我的撸码生活

技术选型

主要考虑到一些接口需要支持的东西,然后从这方面开始选型

首先技术栈是java, 那底层支持就好选了, springBoot + maven搞起来

java测试框架嘛 testng搞起来, 支持多线程多纬度运行测试用例,支持重试,支持数据驱动等等等等

测试报告allure, 和testng完美兼容,且有现成jenkins插件

测试数据yaml保存

http请求restassured 这个http请求库基本上为测试而生

选型基本敲定,从底层到测试报告到ci/cd

版本迭代

1.最初版本 0.0.1

1.1 具体实现方案为编写一个接口api, 该api定义baseUrl, 且通过方法与注解来描述里面的接口,返回一个restassured 的response, 然后通过代理类解析该接口生成一个具体实现类注入spring容器中

1.2 测试用例继承AbstractTestNGSpringContextTests, 用@SpringBootTest开启spring容器

1.3 测试用例可用@Autowired注入接口类,然后像调用普通方法一样调用

大体代码如下:

接口定义类:

@HttpServer(baseUrl = "https://testplatform.com.cn/testool-api")
@Filters({RestAssuredLogFilter.class, RequestLoggingFilter.class, ResponseLoggingFilter.class})
public interface UserApi {

@Get(url = "/menu/list", descriptoin = "获取所有菜单")
Response getMenus();

@Get(url = "/menu/byPhone", descriptoin = "根据手机号码查询菜单")
Response byPhone(@ParamsForm("phone") String phone);

@Get(url = "/role/list", descriptoin = "分页查询角色")
Response getRoleListPage(@Head("pageNo") String pageNo, @Head("pageSize") String pageSize, @ParamsForm("roleName") String roleName);

@Post(url = "/role/add", descriptoin = "增加用户角色")
Response addRole(@ParamsJson String json);
}

测试用例类:

@Features("ces")
@Stories("测试Http")
public class TestoolApiTest extends BaseApiTestCase {
@Autowired
private UserApi userApi;

@Severity(SeverityLevel.CRITICAL)
@Description("测试api测试")
@Title("TestoolUserApi")
@Test(groups = "test-groups-1", dataProvider = "loadDataParams")
//失败重试两次
@RetryCount(count = 2)
@DataParams({"1,10,"})
public void getRoleListPage(String pageNo, String pageSize, String roleName) {
Response response = userApi.getRoleListPage(pageNo, pageSize, roleName);
response.then()
.statusCode(200)
.body("code", equalTo(200))
.body("success", equalTo(true));
}
}

baseTestCase主要做一些继承和参数化

//properties属性指定本地测试需要用到的配置文件
//本地运行时,需要设置properties值为具体的某个配置文件
@SpringBootTest(properties = {"spring.profiles.active=local"})
//通过maven命令运行时需要把该参数去掉
//@SpringBootTest
public abstract class BaseTestCase extends AbstractTestNGSpringContextTests {
@DataProvider
public static Object[][] loadDataParams(Method method){
DataParams dataParams = method.getAnnotation(DataParams.class);

AssertUtils.notNull(dataParams, method.getName() + "方法添加了DataProvider数据驱动,没有添加@DataParams注解");

String[] values = dataParams.value();

String split = dataParams.splitBy();

List<Object[]> result = Lists.newArrayList();

for (int i = 0; i < values.length; i++) {
String[] v1 = values[i].split(split, -1);
result.add(v1);
}

return wildcardMatcher(Utils.listToArray(result));
}
}

主要注解意义:

- @HttpServer: 接口类上注解,非必填,可设置整个接口类的baseUrl
- @Filters: 用于传递reat-assured过滤器注解,接收一个io.restassured.filter.Filter类数组,
如果注释在类上,则整个类接口都会使用该注解里面的过滤器,如果注解在方法上,则作用域该方法
- @Get/@Post: 定义接口请求方法,作用于方法上,需要设置接口请求url,接口简介,如果url为带http/https则最终请求url为该url
如果不带http/https,则最终请求urlbaseUrl + url,该注解可设置请求类型:content-type与请求字符编码集charset
- @HeadMap 作用于请求参数上,设置请求头,参数类型:Map
- @Head 作用于请求参数上,设置请求头,需要设置value值,请求时候valuekey,参数为value,可设置多组,如果
同时设置@HeadMap@Head则会合并为一个map
- @paramsForm 作用于请求参数上,设置请求参数,为k-v类型,注解vaule为请求k,参数为v,可多个,合并处理
- @ParamsJson 作用于请求参数上,设置请求参数,json模式,请求方式为json
- @ParamsMap 作用于请求参数上,设置请求参数,参数类型:Map

然后通过testng 的xml配置文件配置运行

2.迭代1.0.0(直接跳到现在版本,中间迭代太多)

主要问题是写一个用例耗时太多,需要改太多东西, 然后就有了思考空间, 把不需要的细节隐藏

2.1 引入yaml描述接口与用例

name: login
type: json
description: 登录成功
url: /login
method: POST
headers:
x-request-client-imei: "222222222222"
requests:
{
"phone": "${phone, 13000000001}",
"code": "0000"
}
setup:
- method: createTimestamp
- method: setUptest
args: ${request}
teardown:
- method: teardowm
args: args1111, args222
- method: teardowm1
args: ${response}, ${timestamp}
onFailure:
- method: onFailure
args: args1111, args222
validate:
eq: ["result": 0, "error_code": "0"]
notNull: ["data.token"]
plugin:
- method: "teardowm"
args: args1111, ${orderId}
saveGlobal: ["orderId": "response.data.token"]
saveMethod: ["orderId": "response.data.token"]
saveClass: ["orderId": "response.data.token"]
saveThread: ["orderId": "response.data.token"]
parameters:
- name: login-phone-unregistered
description: 手机号未注册
headers:
requests:
"phone": "13000000009"
"code": "0000"
validate:
eq: ["result": 1]
- name: login-phone-not-found
description: 手机号不存在
requests:
"phone": "10000000000"
"code": "0000"
validate:
eq: ["result": 1]
- name: login-code-length-error
description: 验证码长度错误
requests:
"phone": "10000000000"
"code": "000"
validate:
eq: ["result": 1]
- name: login-code-error
description: 验证码错误
requests:
code: "0001"
validate:
eq: ["result": 1]

2.2 BaseHttpClient , Response类的一些处理

- BaseHttpClient对象方法
wait(),wait(TimeUnit unit, long interval) 用于设置请求接口前的等待时间
saveAsk(),saveGlobal(), saveTest(),saveSuite() 用于往不同生命周期保存一个缓存,saveAsk为该请求生命周期
doHttp(BaseModel model)接口调用,入参为model,SINGLE模式时候直接传入方法入参BaseModel即可
doHttp(String modelName)接口调用,入参为modelName, MULTIPLE模式时候传入modelName即可
- Response对象方法
then(): 语法糖,无特殊意义,只用作链式调用标明
statusCode(): 用于断言接口返回code
validate(): 断言方法
eq(): 硬编码断言相等
eqByPath(): 硬编码断言相等,值取jsonpath,xpath
validatePlugin(): 硬编码断言,用于调用方法
saveGlobal(), saveTest(),saveSuite() : 结果保存不同维度方法
onFailure(BaseFailHandle failHandle): validate()断言失败后会执行的方法,所以必须在validate()方法后调用,入参为BaseFailHandle接口,需要实现该接口并且重写handle(T t)方法
onFailure(Class clazz): 同上
extract(): 用于取值
processor(BaseProcessorHandle processorHandle)
processor(Class clazz):用于该调用该接口后一些自定义处理,如订单行程需要一分钟,入参为BaseProcessorHandle接口,需要实现该接口并且重写processor(T t)方法
wait(): 接口执行完后等待时间
done(): 用于处理结束,抛出validate()异常,如没吊用extract()方法取值的话该方法为链式调用结尾必须调用
auto(): 自动解析yaml文件所有内容
auto(int httpStatusCode): 自动解析yaml文件所有内容,手动设置httpStatusCode
autoExcludeDone(): 自动解析yml文件, 但是不会自动调用done()方法结束,需要手动调用done结束,主要用于给该http请求添加更多的自定义处理

2.3 然后testng类configuration配置方法不支持参数化,对此进行的一些加强处理

@ApiBeforeMethod
@ApiBeforeClass
@ApiBeforeSuite
@ApiAfterMethod
@ApiAfterClass
@ApiAfterSuite
主要结合@DataModel, @DataFile, @DataParams 当配置方法入参使用

2.4 数据驱动核心注解@DataModel

/**
*
* <p>数据驱动 yaml模式
* <p> 当Format= SINGLE, vaule = {"login", "login1"}
* 表示该用例是单接口模式
* 用例入参模型为: BaseModel
* 入参值为yaml文档name对应的login与login1和这两个name下的所有parameters
* eg.
* @DataModel(value = {"login", "login1"}, format = DataModel.Format.SINGLE)
* public void login(BaseModel model) {
* apiClient.doHttp(model).auto();
* }
*
* <p> 当Format= MULTIPLE, vaule = {"login", "profile"}
* 表示该用例是业务流模式,只会运行一次,业务对应入参可从
* 用例入参模型为: MultipleModel
* value可省略
* eg.
* @DataModel(format = DataModel.Format.MULTIPLE)
* public void login1(MultipleModel model) {
* driverApiClient.doHttp(model.getModel("login")).auto();
* driverApiClient.doHttp(model.getModel("profile")).auto();
* }
*
*/

@Retention(RetentionPolicy.RUNTIME)
@Target({java.lang.annotation.ElementType.METHOD})
public @interface DataModel {
/** 取yaml的name */
String[] value() default {};

/** yaml文件名字,支持多个文件引入,默认路径为 resources下的data.yml */
String[] path() default {"data.yml"};

/** 模式 */
DataModel.Format format() default Format.MULTIPLE;

enum Format
{
/**单接口**/
SINGLE,
/**串行**/
MULTIPLE
}

2.5 spring功能支持多环境集成与中间的一些配置解释

application-qa.yml,application-uat.yml 区分环境配置文件,最终会根据使用环境默认合并到application.yml

- application.yml

- notification节点: 配置通知类型
- retry节点: 配置用例失败重试

- application-qa.yml,application-uat.yml

- httpurl节点: 配置接口层接口类上面@HttpServer注解中baseurl代表值baseurl直接用${driverapi.url}调用即可

2.6 测试结果通知支持钉钉,邮件等,通过application.yml notification节点配置

2.7 参数化说明

yaml模板中支持参数化|jsonpath|xpath等写法,如${phone, 13000000001}, 该用法为该参数化值设置一个default,如果缓存中无该值,那就取default
jsonpath一般用于validatesaveGlobal,用于取返回值校验与保存
setup支持${response}参数化,${response}会转换成BaseModel
teardown支持${response}参数化,${response}会转换成Response

2.8 因为是springboot架构, 可直接集成JdbcTemplate,redisTemplate,mongoTemplate等

2.9 两个生命周期监听器用于拓展

public interface HttpPostProcessor extends PostProcessor{

/**
* http请求之前处理器
* @param context
*/

void requestsBeforePostProcessor(HttpContext context);

/**
* http请求之后处理器
* @param context
*/

void responseAfterPostProcessor(HttpContext context);


/**
* http请求后 对response对象进行各种处理后的处理器
* 在{@link Response done()}内调用
* @param context
*/

void responseDonePostProcessor(HttpContext context);
public interface TestNgLifeCyclePostProcessor extends PostProcessor{

/**
* 测试方法执行前执行
* @param result
*/

void onTestMethodStartBeforePostProcessor(ITestResult result);

/**
* 测试方法执行成功后执行
* @param result
*/

void onTestMethodSuccessAfterPostProcessor(ITestResult result);


/**
* 测试方法执行失败后执行
* @param result
*/

void onTestMethodFailureAfterPostProcessor(ITestResult result);

/**
* 跳过测试方法后执行
* @param result
*/

void onTestMethodSkippedAfterPostProcessor(ITestResult result);

/**
* 在实例化测试类之后且在调用任何配置方法之前调用
* @param context
*/

void onTestClassInstantiationAfterPostProcessor(ITestContext context);

/**
* 在运行所有测试并调用其所有配置方法之后调用
* @param context
*/

void onAllTestMethodFinishAfterPostProcessor(ITestContext context);

/**
* suite执行之前执行 对应test.xml suite标签
* @param suite
*/

void onSuiteStartBeforePostProcessor(ISuite suite);

/**
* suite执行后执行 对应test.xml suite标签
* @param suite
*/

void onSuiteFinishAfterPostProcessor(ISuite suite);

/**
* 配置方法运行前执行(配置方法: beforeTest,AfterTest等)
* @param result
*/

void onConfigurationStartBeforePostProcessor(ITestResult result);

/**
* 配置方法执行成功时执行
* @param result
*/

void onConfigurationSuccessAfterPostProcessor(ITestResult result);

/**
* 配置方法执行失败时执行
* @param result
*/

void onConfigurationFailureAfterPostProcessor(ITestResult result);

/**
* 配置方法跳过时执行
* @param result
*/

void onConfigurationSkipAfterPostProcessor(ITestResult result);

2.10 完美兼容allure注解

2.11 完美兼容testng用法, 只做testng增强

2.12 完美兼容maven, 指定testng用例执行, 支持多种testng运行纬度: XML Files, Groups, Parallel,verbosity,’testnames’ in test tag

2.13 提供com.ly.core.actuator.TestNgRun 编码方式运行用例

2.14 提供一个har格式转换为yaml用例数据(charles导出.har文件转换为yaml格式)

2.15 jenkins支持

3. example

3.1 先编写yaml (yaml 默认放在resource目录下,也可直接指定路径)

testCase:
- name: login
description: 登录
type: json
url: /v1/security/login
method: POST
setup:
- method: createTimestamp
- method: setUptest
args: ${request}
headers:
timestamp: '1589441750400'
os: android9
content-type: application/json;charset=UTF-8
ver: 2.2.0
requests:
code: '0000'
phone: '13000000001'
validate:
notNull: [result]
eq: [result: 0]
len: [result: 1]
hasKey: [data: token]
hasValue: [data: 4]
saveMethod: ["token": "data.token"]
teardown:
- method: teardowm
args: args1111, args222
- method: teardowm1
args: ${response}, ${timestamp}
parameters:
- name: login-phone-unregistered
description: 手机号未注册
requests:
"phone": "13000000009"
"code": "0000"
validate:
eq: ["result": 1]
- name: login-phone-not-found
description: 手机号不存在
requests:
"phone": "10000000000"
"code": "0000"
validate:
eq: ["result": 1]
- name: login-code-length-error
description: 验证码长度错误
requests:
"phone": "10000000000"
"code": "000"
validate:
eq: ["result": 1]
- name: login-code-error
description: 验证码错误
requests:
code: "0001"
validate:
eq: ["result": 1]
- name: login-phone-isNull
description: phone字段不存在
requests:
"phone": null
"code": "0000"
validate:
eq: ["result": 1]

- name: index
description: 首页
type: form
url: /v1/driver/index
method: GET
headers:
authorization: ${token}
timestamp: '1589441751257'
os: android9
ver: 2.2.0
requests: {}
validate:
notNull:
- result
eq:
- result: 0

3.2 编写apiClient, http.test.url写在配置文件中

@HttpServer(baseUrl = "${http.test.url}")
@Filters({RestAssuredLogFilter.class})
public interface DefaultApiClient extends BaseHttpClient{
}

3.3 编写用例

@Story("登录模块接口")
public class ExampleApiTestCase extends BaseDefaultApiTestCase {

@DataModel(value = {"login"},
format = DataModel.Format.SINGLE,
path = {"example.yml"})
@ApiBeforeClass
public void beforeClass(BaseModel model) {
System.out.println("===========beforeClass============: " + model);
}

@DataModel(value = {"login"},
format = DataModel.Format.SINGLE,
path = {"example.yml"})
@ApiAfterMethod
public void afterMethod(BaseModel model) {
System.out.println("=============afterMethod==========" + model);
}



@Severity(SeverityLevel.CRITICAL)
@Description("登录")
@Test(groups = "example")
@DataModel(value = {"login"},
format = DataModel.Format.SINGLE,
path = {"example.yml"})
public void login(BaseModel model) {
apiClient.doHttp(model).auto();
}

@Severity(SeverityLevel.CRITICAL)
@Description("获取司机信息")
@Test(groups = "example")
@DataModel(format = DataModel.Format.MULTIPLE, path = "example.yml")
public void order(MultipleModel model) {
apiClient.doHttp("login") //调用yaml中name为 login的接口
.processorByExpr("token", RedisDelProcessorCallback.class) //调用完做一些处理
.eqByPath("${result}", 0) //断言
.saveMethod("token", "token") //保存作用域为method的缓存
.onFailure(CancelFailHandle.class) //如果失败执行失败兜底处理
.done();// 结束

apiClient.doHttp("index").auto();
}

3.4 testng.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd">
<!--tests级别:不同test tag下的用例可以在不同的线程执行,相同test tag下的用例只能在同一个线程中执行。-->
<!--classs级别:不同class tag下的用例可以在不同的线程执行,相同class tag下的用例只能在同一个线程中执行。-->
<!--methods级别:所有用例都可以在不同的线程去执行。-->
<!--thread-count: 并发线程数-->
<suite name="自动化">
<test verbose="5" name="example" >
<groups>
<!--groups分组-->
<define name="test">
<include name="example" />
</define>

<!--运行的groups-->
<run>
<include name="test" />
</run>
</groups>
<classes>
<class name="com.example.ExampleApiTestCase" />
</classes>
</test>
</suite>

3.5 测试报告 (随便搞了个)

4.写在最后

如果小伙伴有兴趣的话我把业务代码清理下,开源, 源码大概1w多行吧,里面各种其他处理,也希望能收到各位宝贵的意见和建议

5. 开源地址

https://github.com/luoylove/api-test
by. 12.17