作者:周栒 编辑:毕小烦

接口自动化测试是质量保障体系中非常重要的一环,业内也有很多的开源的工具和框架,但是在面对真实测试应用时,为了更加方便的编写、执行和管理测试用例,我们需要对这些框架整合,打造更加切合自己需求的自动化框架。本文将介绍如何设计一款简单的测试框架。

1. 什么是测试框架

想了解测试框架就一定得知道 xUnit。

wikipedia 这么介绍的:

xUnit 是几个单元测试框架的统称,这些框架的结构和功能源自 Smalltalk 的 SUnit。 SUnit 由 Kent Beck 于 1998 年设计,采用高度结构化的面向对象风格编写,可轻松用于 Java 和 C# 等当代语言。在 Smalltalk 中引入该框架后,Kent Beck 和 Erich Gamma 将该框架移植到 Java,并获得了广泛的欢迎,最终在当前使用的大多数编程语言中取得了进展。 许多这些框架的名称是“SUnit”的变体,通常用其预期语言名称中的第一个字母(或多个字母)替换“S”(Java 为“JUnit”,R 为“RUnit”等。 )。这些框架及其通用架构统称为“xUnit”。

所有 xUnit 框架都共享以下基本组件架构,并具有一些不同的实现细节:

  • Test Runner :测试的运行器;
  • Test Case :测试用例,所有的单元都继承此类;
  • Test Fixtures : 用于管理测试用例的执行;
  • Test Suites :测试套件,用来编排测试用例;
  • Test Execution:测试执行,管理测试用例的顺序;
  • Test Result Formatter:测试结果,具备相同的格式,可被整合;
  • Assertions:断言,对测试的结果进行逻辑判断,断言失败通常会引发异常,从而中止当前测试的执行。

目前市面上存在很多开源的测试框架,诸如 JUnit、Pytest、TestNG 等,它们都是遵循 xUnit 体系设计出的测试框架,给我们提供了上述的一系列能力;

既然已经有了这些框架,为什么还要再设计什么新框架呢?

因为我们在做接口测试时,还有其他工作要做,比如需要对 API 发起请求和处理响应结果,需要处理一些配置文件等等,这都要自研或引入第三方的库。

除此之外还有些问题,比如:

  • 学习成本:参与者需要有代码基础,学习成本较高;
  • 数据构造:数据不是简单的存储在 JSON/YAML 中,需要从某个接口中获取;
  • 团队协作:不同成员编写自动化脚本风格差异,用例的整合和统一处理比较困难;
  • 维护成本:接口变更一个,用例需要大批量调整;

基于上述问题,为了方便的让更多人能够轻松的去编写和管理用例,我们需要对框架进行进一步封装和集成,提供更加适配当前业务测试的框架。

所以测试框架的目标是什么?

  1. 让无代码基础的人也可以快速上手,约定好用例编写格式,通过配置 YAML 文件就可以完成基本接口测试,统一编写风格也更加利于团队协作。
  2. 支持具有参数依赖的接口,可以进行变量的抽取和参数赋值,解决数据构造的问题。
  3. 集成 Allure 框架生成测试报告,对测试结果进行可视化展示。

    2. 框架的设计思路

    2.1 测试分层

    在接口自动化测试维护过程中,由于测试用例的增加和接口变更导致测试用例的调整,使自动化测试用例的维护非常麻烦,我们可以将接口对象、测试步骤、测试用例进行分离,单独进行描述和维护,从而尽可能地减少用例的维护成本。
    image.png
    比如分成:
  • 接口层:对接口信息进行独立管理,可以编写多个 YAML 文件;
  • 用例层:
    • 测试步骤层:是测试步骤的集合,每一个测试步骤对应一个接口的请求以及测试结果的断言;
    • 测试用例层:调用「接口层」 的 YAML 文件中的接口并对结果断言,每条测试用例应该是都可以独立运行的;
  • 配置层:
    • 当测试用例数量比较多以后,为了方便实现批量运行,「配置层」添加「测试用例层」路径指定运行用例。
    • 在实际项目中,会存在不同的运行环境,为了兼容一套用例运行多个环境,需要在「配置层」添加环境管理。

      2.2 约定配置

      1. 接口对象

      为了更好地对接口描述进行管理,可以把同类接口放在同一 YAML 文件中管理。可以自定义接口对象 YAML 文件的存储位置,在该目录下新建文件进行接口模块的区分。

如下所示:
image.png

2. 测试步骤和测试用例

后面扩展定义的数据库操作也可放在测试步骤层:
image.png

3. 配置文件

后续新增数据库连接信息也可放在 config.yml 中:
image.png

2.3 变量机制

在做接口测试时,我们在很多地方需要对参数进行声明和引用,这时候就需要设计变量机制:

  1. 测试用例的驱动参数,它的作用域覆盖整个测试用例。
  2. 在某个测试步骤中提取特定的响应参数,并赋值给指定的变量名。该操作也常被成为参数关联。提取的参数变量类似于 session 参数,作用域为当前步骤及之后的步骤。
  3. 在单个测试步骤下声明的 variables 是测试步骤局部变量,作用域仅限当前步骤。各个测试步骤的变量相互独立,互不影响。
  4. 在接口自动化中,很多参数需要唯一,这时候我们可以提供系统变量生成随机数,解决唯一参数的构建,如:时间戳,随机手机号等,它的作用域是覆盖整个测试用例,在测试用例的所有地方都可以引用。

    2.4 参数提取

    要能够基于参数提取机制实现响应结果字段提取和参数关联。在实际业务场景中,很多时候存在参数关联的情况,即当前接口请求参数来自于之前接口的响应结果。

引用方式:通过 JSON 路径去提取相应的值。

2.5 结果校验

结果校验(又称断言)是指针对 API 响应结果进行预期结果校验。这是测试用例中的重要组成部分,可以对测试用例在运行过程中是否得到了预期结果进行校验,例如对响应状态码进行断言,以及对响应 JSON 中的具体字段进行断言。

断言方式:使用 JUnit 的 assertAll 断言

3. 测试框架的实现

3.1 接口层

设计思路:
设计一款简单的接口自动化测试框架 - 图5
具体实现:

1、HttpRquest 类,用于发起 HTTP 请求并返回结果。

  1. public Response run(ArrayList<String > actualParam) throws IOException {
  2. String baseUrl;
  3. HashMap<String,String> finalHeaders = new HashMap<>();
  4. if (this.url.contains("http")){
  5. baseUrl="";
  6. }else {
  7. baseUrl = load("src/main/resources/config.yml").getBaseUrl();
  8. }
  9. String runUrl=baseUrl+this.url;
  10. String runBody=this.body;
  11. HashMap<String ,String > finalQuery=new HashMap<>();
  12. /**
  13. * 全局变量替换
  14. */
  15. if (query!=null){
  16. finalQuery.putAll(Replace.relaceMap(query, Global.getVariable()));
  17. }
  18. if (headers!=null){
  19. finalHeaders.putAll(Replace.relaceMap(headers, Global.getVariable()));
  20. }
  21. runBody= Replace.relaceString(runBody, Global.getVariable());
  22. runUrl= Replace.relaceString(runUrl, Global.getVariable());
  23. /**
  24. * 内部变量替换
  25. */
  26. if (param !=null&&actualParam!=null&& param.size()>0&&actualParam.size()>0){
  27. for (int i = 0; i < param.size() ; i++) {
  28. actionVariables.put(param.get(i),actualParam.get(i));
  29. }
  30. if (query!=null){
  31. finalQuery.putAll(Replace.relaceMap(query, actionVariables));
  32. }
  33. runBody= Replace.relaceString(runBody,actionVariables);
  34. runUrl= Replace.relaceString(runUrl,actionVariables);
  35. }
  36. /**
  37. * 发起请求返回结果
  38. */
  39. RequestSpecification requestSpecification=given().log().all();
  40. if (finalHeaders!=null){
  41. requestSpecification.headers(finalHeaders);
  42. }
  43. if (finalQuery!=null&&finalQuery.size()>0){
  44. requestSpecification.formParams(finalQuery);
  45. }
  46. if (runBody!=null){
  47. requestSpecification.body(runBody);
  48. }
  49. response=requestSpecification.request(method,runUrl).then().log().all().extract().response();
  50. return response;
  51. }

2、定义 ApiObject 类,将一个 API yaml文件反序列化成 Api Object对象。

  1. public static ApiObject load(String path) throws IOException {
  2. ObjectMapper objectMapper=new ObjectMapper(new YAMLFactory());
  3. return objectMapper.readValue(new File(path), ApiObject.class);
  4. }

3、定义ApiLoader 类,用来加载 api 对象和获取接口请求,提供 load 方法,将 api 中的所有 yaml 文件的接口对象加载到 apis 列表中。

  1. public static void load(String dir) {
  2. Arrays.stream(new File(dir).listFiles()).forEach(path->{
  3. if (path.getAbsolutePath().contains(".yaml") || path.getAbsolutePath().contains(".yml")) {
  4. try {
  5. apis.add(ApiObject.load(path.getAbsolutePath()));
  6. } catch (IOException e) {
  7. e.printStackTrace();
  8. }
  9. } else {
  10. load(path.getAbsolutePath());
  11. }
  12. });
  13. }

提供 getRequests 方法,在用例层根据接口集合的名称和接口名称执行 request

  1. public static HttpRquest getRequests(String apiName, String requestName){
  2. final HttpRquest[] apiRquests ={new HttpRquest()};
  3. apis.stream().filter(api -> api.getName().equals(apiName)).forEach(api -> apiRquests[0] = api.getApis().get(requestName));
  4. if (apiRquests[0]!=null){
  5. return apiRquests[0];
  6. }
  7. return null;
  8. }

3.2 用例层

设计思路:
设计一款简单的接口自动化测试框架 - 图6
具体实现:

1、定义 StepModel 类,用来存储运行 testcase yaml 反序列化出来的 step 单元。

  1. public StepResult run(HashMap<String, String> testCaseVariables) throws Exception{
  2. /**
  3. * 替换入参变量
  4. */
  5. if (actualParam != null) {
  6. finalActualParam.addAll(Replace.resolveList(actualParam, testCaseVariables));
  7. }
  8. /**
  9. * 执行request
  10. */
  11. Response response = ApiLoader.getRequests(api, request).run(finalActualParam);
  12. /**
  13. * 保存用例层变量
  14. */
  15. if(save !=null){
  16. save.forEach((variablesName, path) -> {
  17. String value = response.path(path).toString();
  18. stepVariable.put(variablesName, value);
  19. });
  20. }
  21. /**
  22. * 保存全局变量
  23. */
  24. if (this.saveGlobal != null) {
  25. saveGlobal.forEach((variablesName, path)->{
  26. HashMap<String ,String > h=new HashMap<>();
  27. if (variablesName.equals("Cookie")){
  28. String cookie=path+"="+response.getCookie(path);
  29. h.put(variablesName,cookie);
  30. }else {
  31. String value = response.path(path.toString());
  32. h.put(variablesName,value);
  33. }
  34. Global.getVariable().putAll(h);
  35. });
  36. }
  37. /**
  38. * 测试结果
  39. */
  40. if (asserts != null) {
  41. asserts.stream().forEach(assertModel -> {
  42. assertList.add(() -> Assertions.assertEquals(response.path(assertModel.getEq().get(0)).toString(), assertModel.getEq().get(1)));
  43. });
  44. stepResult.setAssertList(assertList);
  45. stepResult.setStepVariables(stepVariable);
  46. }
  47. return stepResult;
  48. }

2、定义 TestCaseModel 类,遍历执行用例下的测试步骤并对测试结果进行统一断言。

  1. public void run(){
  2. /*
  3. * 执行测试步骤
  4. */
  5. steps.forEach(step->{
  6. try {
  7. StepResult stepResult = step.run(testCaseVariables);
  8. if (stepResult.getStepVariables().size() > 0) {
  9. testCaseVariables.putAll(stepResult.getStepVariables());
  10. }
  11. if (stepResult.getAssertList().size() > 0) {
  12. assertList.addAll(stepResult.getAssertList());
  13. }
  14. } catch (Exception e) {
  15. e.printStackTrace();
  16. }
  17. });
  18. assertAll(assertList.stream());
  19. }

3、定义 TestCaseLoader 类,反序列化 testcase下所有的 yaml 文件,与 ApiLoader 类相似,不再赘述。

3.3 执行用例

定义 TestRunner 类,用来加载并运行 testcase 下的所有测试用例。

利用 JUnit5 ParameterizedTest 参数化批量运行用例,如下所示:

  1. public class TestRunner {
  2. private static final Logger logger = LoggerFactory.getLogger(TestRunner.class);
  3. @ParameterizedTest(name = "{index}{1}")
  4. @MethodSource
  5. void apiTest(TestCaseModel apiTestCaseModel,String name) throws Exception{
  6. logger.info("【用例开始执行】");
  7. logger.info("用例名称:"+name);
  8. apiTestCaseModel.run();
  9. }
  10. static List<Arguments> apiTest() throws IOException {
  11. ConfigLoader configLoader= ConfigLoader.load("src/main/resources/config.yml");
  12. ApiLoader.load(configLoader.getApiPath());
  13. List<TestCaseModel> testcase = TestcaseLoader.loadDir(configLoader.getTestcaePath());
  14. List<Arguments> argumentsList=new ArrayList<>();
  15. testcase.forEach(apiTestCaseModel->{
  16. argumentsList.add( arguments(apiTestCaseModel,apiTestCaseModel.getName()));
  17. });
  18. return argumentsList;
  19. }
  20. }

3.4 测试报告

复用 JUnit5Allure 框架生成测试报告。

步骤如下:
STEP 1. 下载 allure2 并配置环境变量
官网:https://allure.qatools.ru/
STEP 2. 配置 allure 依赖:

  1. <dependency>
  2. <groupId>io.qameta.allure</groupId>
  3. <artifactId>allure-junit5</artifactId>
  4. <version>2.13.2</version>
  5. <scope>test</scope>
  6. </dependency>

STEP 3. 执行用例生成测试数据到 ./allure-result
image.png
STEP 4. 查看报告

  1. allure serve ./allure-result

image.png

4. 总结

本文主要介绍了一款基础的接口测试框架的设计思路和实现细节,希望对于想自己动手设计开发接口测试框架的同学有所帮助。

参考资料:

(完)

微信搜索“毕小烦”或者扫描下面的二维码,即可订阅我的微信公众号
image.png
如果文章对你有帮助,记得留言、点赞、加关注哦!