接口测试 [接口测试平台一期] 接口测试用例参数化方案

bauul · December 25, 2017 · Last by yueyawan replied at October 09, 2018 · 5173 hits
本帖已被设为精华帖!

缘由

12月23号去上海参加了论坛举办的活动,干货蛮多的,感觉别人家做的都好腻害,
其中Lego接口自动化测试印象挺深的,通过配置文件的方式生成接口测试用例,
因为恰好我们公司也准备做接口自动化的部分,之前还没有这部分😅

依赖

  1. rest-assured
  2. testng

两种方案

代码实现

这是我在参加活动前的方案,有同事推荐retrofit不错,自己写了一下,感觉不方便
在论坛上看到的rest-assured挺不错的,用起来方便的

@Slf4j
@Listeners({InterfaceFailureHandle.class, Retry.class})
public abstract class BaseInterfaceTest {

public static Map<String, String> testParametersMap = new HashMap<>();

static {

/** 公共参数 **/
testParametersMap.put("appKey", "android_111");
testParametersMap.put("appTimestamp", String.valueOf(System.currentTimeMillis()));
testParametersMap.put("appTypeId", "0");
testParametersMap.put("cookieId", "eee");
testParametersMap.put("countryCode", "SA");
testParametersMap.put("currency", "SAR");

}

@BeforeClass
@Parameters({"baseUrl", "appVersion"})
public void init(@Optional String baseUrl, @Optional String appVersion) {
log.debug("init");

if (baseUrl == null) {
baseUrl= "http://weekly.test.com";
}
if (appVersion != null) {
testParametersMap.put("appVersion", appVersion);
}

RestAssured.baseURI = baseUrl;
RestAssured.enableLoggingOfRequestAndResponseIfValidationFails();
RestAssured.requestSpecification = new RequestSpecBuilder().build().accept(JSON).contentType(JSON);
}

}
@Slf4j
@Listeners({InterfaceFailureHandle.class})
public class LoginNew extends BaseInterfaceTest{

@Test
public void loginWithUerNamePassword() {
log.debug("actions");

JSONObject jsonObject = new JSONObject();
testParametersMap.put("userName", "carl@163.com");
testParametersMap.put("password", "kkkkkk");
jsonObject.putAll(testParametersMap);
testParametersMap.put("sign", SignGen.getSign(jsonObject, SignGen.appSecret));
jsonObject.putAll(testParametersMap);

log.debug(jsonObject.toJSONString());

given().body(jsonObject.toJSONString())
.when().post("/user/login").then()
.body("messageCode", is("0"),
"messageType", is(0));
}
}

参数配置实现

简单学习了一下yaml配置文件的写法,还是没完全搞清楚,
所以我选择写出javabean出来先,然后dump出来,再按dump出来的格式学着具体写

@Data
public class APITestProject {

private String baseUrl;
private Map<String, Object> globalRequestParmeters = new HashMap<>();
private Map<String, APITestSuite> testSuites = new HashMap<>();

}
@Data
public class APITestSuite {

private String description;
private List<APITestCase> testCaseList = new ArrayList<>();
private Map<String, Object> testSuiteParameters = new HashMap<>();
}
@Data
public class APITestCase {

private String name;
private String description;
private String apiUrl;
private Map<String, String> sqlCommands = new HashMap<>();
private Map<String, Object> requestParameters = new HashMap<>();
private Map<String, Map<String, Object>> resultVerify = new HashMap<>();
}

一个project下面有多个 testsuite,一个testsuite下面有多个用例,然后全局参数,测试套参数,用例参数,校验结果

!!APITestProject
baseUrl: http://weekly.test.com
#全局参数
globalRequestParmeters:
appVersion: '6.12'
appTypeId: '0'
countryCode: SA
cookieId: e0dc298b-2e2d-4f14-ab80-cfcce4471679
appKey: android_lk98f83
currency: SAR
lang: '0'
appTimestamp: '1514204071822'
terminalType: '1'
#测试套集合
testSuites:
firstSuite:
description: testSuiteDesc
#测试用例集合
testCaseList:
- apiUrl: /user/login
description: kkk
name: loginNew
requestParameters:
password: kkkkkk
userName: clark@163.com
resultVerify:
#结果校验
is:
messageType: 0
messageCode: '0'
#sql,在测试前或测试后做数据准备或还原,未完成
sqlCommands: {}
#测试套参数集合
testSuiteParameters: {}
@Slf4j
@Listeners({InterfaceFailureHandle.class, Retry.class})
public class APITestExecutor {

private static Map<String, Object> testParametersMap = new HashMap<>();
private static APITestProject apiTestProject;

static {

Yaml yaml = new Yaml();
try {
apiTestProject = yaml.loadAs(new FileInputStream(new File("src\\main\\resources\\InterfaceTest.yaml")), APITestProject.class);
testParametersMap.putAll(apiTestProject.getGlobalRequestParmeters());
} catch (FileNotFoundException e) {
e.printStackTrace();
log.debug(e.getMessage());
}
}

@BeforeMethod
@Parameters({"baseUrl", "appVersion"})
public void init(@Optional String baseUrl, @Optional String appVersion) {
log.debug("init");

if (baseUrl == null) {
baseUrl = apiTestProject.getBaseUrl();
}
if (appVersion != null) {
testParametersMap.put("appVersion", appVersion);
}

RestAssured.baseURI = baseUrl;
RestAssured.enableLoggingOfRequestAndResponseIfValidationFails();
RestAssured.requestSpecification = new RequestSpecBuilder().build().accept(JSON).contentType(JSON);

}

@Test
public void executor() {

JSONObject jsonObject = new JSONObject();

Map<String, APITestSuite> testSuites = apiTestProject.getTestSuites();
for (Map.Entry<String, APITestSuite> entry : testSuites.entrySet()) {

List<APITestCase> testCases = entry.getValue().getTestCaseList();
for (APITestCase testCase : testCases) {
testParametersMap.putAll(testCase.getRequestParameters());
jsonObject.putAll(testParametersMap);

testParametersMap.put("sign", SignGen.getSign(jsonObject, SignGen.appSecret));
jsonObject.putAll(testParametersMap);

log.debug(jsonObject.toJSONString());

ValidatableResponse validatable = given().body(jsonObject.toJSONString()).when().post(testCase.getApiUrl()).then();

Map<String, Map<String, Object>> resultVerify = testCase.getResultVerify();

resultVerify.forEach((condition, v) -> {
v.forEach((key, value) -> {
try {
Matchers matches = new Matchers();
Matcher matcher = (Matcher) Matchers.class.getDeclaredMethod(condition, Object.class).invoke(matches, value);

validatable.body(key, matcher);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
}

});
});

validatable.log().all();

}
}

}

}

读取yaml文件并遍历,执行测试

对比

代码:

  • 优点是写代码,灵活性高,特殊场景什么的都可以处理;
  • 缺点:如果100,1000条用例的话,重复代码非常多,后期维护可能很心累

配置文件:

  • 优点是结构清晰,轻量级
  • 缺点:未来可能出现特殊场景,随着用例数上去之后,很难通过修改配置文件的方式去兼容更多的场景

问题

  1. 如果yaml文件中有10条用例,在执行时是放在一个@Test方法中执行的,就是说第3条用例失败即全部失败了,如何进一步拆分
  2. rest-assured初始化需要7~8秒的时间,感觉有点长了

解决方案

  1. 通过代码来运行testNG的测试用例,读取用例后,放入队列中,每次执行时读取一条用例即可

    public class APITestRun {

    public static void main(String[] args) {
    for (int i=0; i<APITestUtils.getBlockingQueue().size(); i++) {
    TestNG testNG = new TestNG();
    testNG.setTestClasses(new Class[]{APITestExecutor.class});
    testNG.run();
    }

    }
    }
@Slf4j
@Listeners({InterfaceFailureHandle.class, Retry.class})
public class APITestExecutor {

private APITestCase testCase;

@BeforeMethod
@Parameters({"baseURL"})
public void init(@Optional String baseURL) {
log.debug("init");

if (baseURL == null) {
baseURL = APITestUtils.getApiTestProject().getBaseURL();
}

RestAssured.baseURI = baseURL;
RestAssured.enableLoggingOfRequestAndResponseIfValidationFails();
RestAssured.requestSpecification = new RequestSpecBuilder().build().accept(JSON).contentType(JSON);

testCase = APITestUtils.getTestCase();
}

@Test
public void executor() {

JSONObject jsonObject = new JSONObject();
jsonObject.putAll(APITestUtils.getApiTestProject().getGlobalRequestParmeters());
jsonObject.putAll(testCase.getRequestParameters());

String sign = SignGenerate.getSign(jsonObject, SignGenerate.appSecret);
jsonObject.put("sign", sign);

log.debug(jsonObject.toJSONString());

ValidatableResponse validatableResponse = given().body(jsonObject.toJSONString()).when().post(testCase.getApiUrl()).then();

Map<String, Map<String, Object>> resultVerify = testCase.getResponseVerify();

resultVerify.forEach((condition, v) -> {
v.forEach((key, value) -> {
try {
Matchers matchers = new Matchers();
Matcher matcher = (Matcher) Matchers.class.getDeclaredMethod(condition, Object.class).invoke(matchers, value);
validatableResponse.body(key, matcher);
} catch (Exception e) {
e.printStackTrace();
}
});
});

validatableResponse.log().all();

updateTestCaseInfo();
}

/**
* 更新测试用例的名字和描述信息
*/

public void updateTestCaseInfo() {
TestCaseEvent testCaseEvent = new TestCaseEvent() {
@Override
public void process(TestCaseResult testCaseResult) {
log.debug("name:" + APITestUtils.getCurrentTestCase().getName());
log.debug("description:" + APITestUtils.getCurrentTestCase().getDescription());
testCaseResult.setName(APITestUtils.getCurrentTestCase().getName());
ru.yandex.qatools.allure.model.Description description = new ru.yandex.qatools.allure.model.Description();
description.setValue(APITestUtils.getCurrentTestCase().getDescription());
testCaseResult.setDescription(description);
// testCaseResult.getLabels().add(new Label().withName("testSuite").withValue(""));
}
};

APITestUtils.getAllure().fire(testCaseEvent);
}
}
@Slf4j
public final class APITestUtils {

private static APITestProject apiTestProject;
private static BlockingQueue<APITestCase> blockingQueue = new LinkedBlockingQueue<>();
private static APITestCase currentTestCase;
private static Allure allure = Allure.LIFECYCLE;

private APITestUtils() {

}

static {
Yaml yaml = new Yaml();
try {
// 加载用例文件
apiTestProject = yaml.loadAs(new FileInputStream(new File("src\\main\\resources\\InterfaceTest.yaml")), APITestProject.class);
} catch (FileNotFoundException e) {
e.printStackTrace();
log.debug(e.getMessage());
}

/** 遍历用例并放入队例中 **/
Map<String, APITestSuite> testSuites = apiTestProject.getTestSuites();
for (Map.Entry<String, APITestSuite> entry : testSuites.entrySet()) {
List<APITestCase> testCases = entry.getValue().getTestCaseList();
blockingQueue.addAll(testCases);
}
}

public static BlockingQueue<APITestCase> getBlockingQueue() {
return blockingQueue;
}

public static APITestCase getTestCase() {
currentTestCase = blockingQueue.poll();
return currentTestCase;
}

public static APITestCase getCurrentTestCase() {
return currentTestCase;
}

public static APITestProject getApiTestProject() {
return apiTestProject;
}

public static Allure getAllure() {
return allure;
}

报告

其他小芝麻

  1. TestNG测试注解以及生命周期:
    @BeforeClass(执行一次)
    @BeforeMethod(N个Test 方法执行N次)
    @Test Test方法(此注解可能在类上表示多个,在方法表示一个)
    @AfterMethod(N个Test 方法执行N次)
    @AfterClass(执行一次)

  2. LinkedBlockingQueue
    poll: 若队列为空,返回null。
    remove:若队列为空,抛出NoSuchElementException异常。
    take:若队列为空,发生阻塞,等待有元素。

  3. okhttp EOF Exception
    见=>https://blog.csdn.net/m_xiaoer/article/details/72858895

最后

看别人做的好漂亮,好厉害,自己动手尝试去做的时候,就感觉坑也不少的

附言 1  ·  December 26, 2017

接下来的问题:

  1. 如何把参数传到注解中去?因为使用了allure报告,就比如用例名,用例描述,如何传递给@Description注解?
附言 2  ·  December 26, 2017
  1. 更新了下代码,可以看到用例名和描述信息了,不过Default feature和Default story还没找到,后续再更新,基本需求完成
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 34 条回复 时间 点赞

我也试过用yaml维护接口参数,我的烦恼是编写不方便,写json习惯了,所以我用.json文件维护

—— 来自TesterHome官方 安卓客户端

飞狐 回复

胡总给我点经验啊😊

感觉这样去做的工作量会很大,为啥不在已有的jmeter基础上来做这个呀

战 神 回复

没怎么用过jmeter,帮忙提供更多的信息啊,用jmeter怎么玩啊

bauul 回复

jmeter相当强大啊,可以参数化,可以做服务端性能,可以做接口自动化,也有自带的UI界面,是很完善的接口测试框架&&工具呀,你这个参数化,在jmeter的Beanshell前置处理器里面去做 很方便的。。。

战 神 回复

还可以去看看官方提供API的各种sampler,apache都给你正好啦,而且性能足够完美

战 神 回复

嗯,我学学,谢谢

乐高么,美团点评那个,我跟他讨论半天。

Lucas 回复

bauul #10 · December 26, 2017 作者

@Lihuazhang
可以把编辑预览菜单放到底下吗?这样字码完了(超过一屏),就在底下直接点击编辑或预览了,而现在的情况就是需要滚到最上面点击预览,不方便啊

bauul 回复

我大致是这样的写法,这里主要自动生成case和异常case,正常case 的数据源

bauul #12 · December 26, 2017 作者
飞狐 回复

👍
主要涵盖哪些场景,有没有哪些场景无法覆盖?

#12楼 @carl 这种接口主要针对的是单接口的健壮性及正常结果的验证,业务接口组合我是用另外的框架处理的

—— 来自TesterHome官方 安卓客户端

#12楼 @carl 你可以看下论坛里面得精华帖,我记得有几篇针对接口框架,我感觉还是不错的

—— 来自TesterHome官方 安卓客户端

bauul #15 · December 26, 2017 作者
飞狐 回复

嗯,我看了几个rest-assured写的,还有jmeter写的,可惜我还对jmeter不熟,后面学了再搞

思寒_seveniruby 将本帖设为了精华贴 26 Dec 22:52

我是分享那个Lego接口测试解决方案的,看来影响力还不错啊~

bauul #19 · December 29, 2017 作者
陈永达 回复

感谢,干货满满的分享👍

另外你标题写的“参数化”是“拿用例的方式”,还是每个用例请求过程中,“参数需要替换”的动作啊?

bauul #21 · December 29, 2017 作者
陈永达 回复

我这个现在是“拿用例的方式”,每个请求过程中,参数需要替换我记得那天你也有讲到的,但是我没明白主要是哪种场景

经你这么一问,我似乎想明白了,是同一个用例,但是有不同的参数,实现的测试目的可能不一样,所以需要把请求参数再参数化,是这样吗?

我原来的理解是,对于同一个用例,不同的参数请求,是通过复制多个用例来实现的。
后面会把这些参数放到数据库中,用户可以在浏览器中看到这个用例的详细参数,如果用例失败,可以在浏览器中修改参数,对前端页面不熟悉,还要去做原型设计,这方面有什么快速成型的建议吗?
感谢

bauul 回复

在选取用例部分,我当时说的是,使用testNG的xml配置文件,通过@Parameter的方式,将sql传到TestNG的脚本,来实现测试用例的选取。

在测试用例中,“参数化” 最主要解决的问题是:针对那些可能会失效、会变化、会被删、需要通过一些计算才能得到的参数,进行一些“设计”,每次执行用例的时候,实时执行“设计”来生成参数,增加用例的健壮性。
是解决这样的问题的,毕竟这样的数据如果硬代码,只会常常让测试用例报错,增加维护成本。

“我原来的理解是,对于同一个用例,不同的参数请求,是通过复制多个用例来实现的。” 你理解的是对的,不用测请求目的,是复制多个用例来实现的,我的设计就是,一个用例就是单纯的一个系列的测试目的。用例是最基础的数据,你想要分类,可以在用例的上面来加一层逻辑的分类来实现,我个人觉得没必要在用例这个维度上做过多的设计,一个用例有10个测试目的,我觉得没什么意义,也不够清晰。

前端页面的话,我个人java比较熟Servlet感觉还蛮简单的,Python也有很多容易上手的框架。

bauul 回复

我不太看网站,可能会回的不怎么及时,可以加我微信,你回了微信上告诉我下

bauul #24 · January 02, 2018 作者
陈永达 回复

了解了,非常感谢👍

测试数据很多的时候,yaml一条条写起来不会很麻烦吗。。

bauul #26 · January 08, 2018 作者
cctodd 回复

做了Har文件(通过Fiddler录制出来的)自动转yaml用例了,根据项目做了定制,不用手写参数了

bauul 回复

参考楼主的方案我实践了下,用比较适合回归,比如稳定、简单一点的接口。
我是拿来在线上跑的,没有加入sql查询。

bauul #28 · January 26, 2018 作者
cctodd 回复

也可以加入参数传递的功能,比如A接口依赖B接口的参数或B接口的返回值,坑已踩完,回头我发一下

大神,有空将yaml 文件修改成mysql ,你再发一文吧

bauul #30 · January 29, 2018 作者
bill 回复

哈哈,年后计划,二期工作

楼主,“如何把参数传到注解中去?因为使用了allure报告,就比如用例名,用例描述,如何传递给@Description注解?” 请问下这个问题是怎么解决的呢?

bauul #33 · May 04, 2018 作者
y 回复
  Allure.LIFECYCLE.fire(new TestCaseEvent() {
@Override
public void process(TestCaseResult testCaseResult) {
List<Parameter> parameterList = new ArrayList<>();
Parameter request = new Parameter();
request.setName("Your key");
request.setValue(Your value);
request.setKind(ParameterKind.ARGUMENT);

}
});
bauul [接口测试平台二期] 批量数据支持 中提及了此贴 22 Jun 11:15

你好,能给下最后testng示例的代码吗?多谢

bauul #36 · September 29, 2018 作者
yueyawan 回复

啥?这个吗?

TestNG testNG = new TestNG(false);
testNG.setSuiteThreadPoolSize(1);
testNG.setThreadCount(1);
testNG.setTestClasses(new Class[]{TestExecutor.class});
JsonTestUtils.getTestCase();
testNG.run();
bauul 回复

嗯,我写的是从mysql 读取用例数据,想参考下你的代码

simple 专栏文章:[精华帖] 社区历年精华帖分类归总 中提及了此贴 13 Dec 14:44
需要 Sign In 后方可回复, 如果你还没有账号请点击这里 Sign Up