测试框架设计

价值

  • 简化自动化测试技术
  • 规范领域测试模型
  • 数据驱动与API相结合
  • 自动生成用例:分析的结构化数据,生成代码格式的用例非常复杂,直接保存为数据格式更好的
  • 与云平台对接:数据保存到了数据库的表结构里,数据的传输转换也更适合使用数据

测试框架设计思路

测试框架的核心要素xUnit

  • Test Runner
  • Test Case
  • Test Fixtures
  • Test Suites
  • Test Execution
  • Test Result Formatter
  • Assertions

xUnit框架体系

  • Java:Junit4、TestNG、JUnit5
  • Python:UnitTest、PyTest

几乎所有的语言都是xUnit实现

测试框架的用途

  • 单元测试
  • Web自动化测试Selenium
  • Appium自动化测试Appium
  • 接口自动化测试Requests、Rest-Assured

承载特定领域的测试用例管理

领域建模与抽象封装

  • 业务领域建模
  • 自动化领域建模
  • 抽象为资源对象、操作方法、状态切换
  • Page Object模式

用例表达方式

  • TDD:xUnit
    • junit/testng+po+param,适合测试开发
  • DDT:数据驱动测试(测试服务化)
    • 非测试开发人员(业务测试、产品,研发、甲方),平台化支持(与其他框架对接,自动生成、框架切换、平台调度)
  • ATDD:验收测试驱动开发,代表作RobotFramework
  • BDD:行为驱动开发,代表作Cucumber
  • API:使用领域特定api描述测试,代表作Requests、RestAssured

Api方法已经成为测试框架的主流使用方法

参数化与数据驱动

  • 参数化:将用例的关键数据变成参数以实现批量数据驱动
    • 关键数据变为外部传入参数
    • 执行时使用数据替换参数
  • 数据驱动测试:使用管理良好的外部数据表达测试并驱动测试执行
    • 代码与数据结构
    • 关键测试数据来源于外部数据源

数据驱动常见应用

  • 测试数据的数据驱动
  • 测试步骤的数据驱动
  • 全局配置的数据驱动

数据驱动

  • 数据来源:csv、yaml、xml、db、excel、json
  • 读取数据源返回数组:
    • 基于schema:List<Class>
    • 纯数据:List<HashMap> List[Dict]
  • 利用参数化进行数据与变量的对应

数据格式的选择

优点 缺点
Excel 生成数据方便 二进制文件不利于版本管理
CSV 可使用Excel编辑 表达多层级多类型数据有困难
XML 格式完备 冗长复杂
JSON 格式完备,可读性一般 不能编写注释,格式死板
YAML 格式完备,可读性好

测试数据的数据驱动

接口测试,我们关注的是输入输出的结果是否符合我们的预期,在调用接口的时候,接口是固定的,但是参数是不一致的,下面我们看个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
package com.api.test.apiobject.accounting;

import com.api.test.utils.OauthClientUtils;
import io.restassured.response.Response;
import org.apache.oltu.oauth2.common.exception.OAuthProblemException;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

import java.util.stream.Stream;

import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.params.provider.Arguments.arguments;

/**
* @author jingLv
* @date 2020/09/21
*/
class IsWorkingDayTest {

private String token;

{
try {
token = OauthClientUtils.getApiToken("xxxxx-VQDuKQUG", "85Omh6tOHgwI");
} catch (OAuthProblemException e) {
e.printStackTrace();
}
}

static Stream<Arguments> testData() {
return Stream.of(
arguments("2020-09-21", "00000", "ok", true),
arguments("2020-09-20", "00000", "ok", false),
arguments("2020-10-01", "00000", "ok", false),
arguments("9999-99-99", "00001", "当前日期不再账务系统工作日期库中,无法确定是否是工作日!", null),
arguments("abcdefg", "00001", "入参abcdefg格式错误,应为yyyy-MM-dd", null)
);
}


/**
* 测试用例,参数与返回值
*/
@ParameterizedTest
@MethodSource("testData")
@DisplayName("判断传入的日期是否是工作日")
void testIsWorkingDayApi(String date, String code, String message, Boolean data) {
Response isWorkingDayResponse = IsWorkingDayApiObject.successRequestIsWorkingDayApi(date, token);
assertAll("result assertions",
() -> assertEquals(isWorkingDayResponse.path("code"), code),
() -> assertEquals(isWorkingDayResponse.path("message"), message),
() -> assertEquals(isWorkingDayResponse.path("data"), data)

);
}
}

这是一个传入日期判断是否是工作日的接口,根据传入不同的数据,对应不同的结果,我们结合Junit5参数化的功能,只要设定好输入和输出的数据,在传入接口测试时,进行断言,我们就可以快速的测试接口的多种情况。

测试步骤的数据驱动

核心技术概念

  • 模板替换:使数据源中的数据动态化
  • 变量引用:可以导出变量并在后续步骤中引用
  • 自定义扩展:支持自定义的编程逻辑
  • xUnit测试封装:用例、套件、执行、断言、装置等等

具体实现

  1. 建立自动化领域模型

  2. 使用yaml文件管理模型数据(用例、配置)

    • 测试步骤:基本自动化领域模型,业务模型的设计,以下几种方式
      • 方式一:通用方法及带有参数,这种方式,会使模型的内容繁多冗长
        1
        2
        3
        4
        5
        6
        7
        8
        9
        name: 企业微信新增通讯录成员
        deascription: 企业微信新增通讯录成员
        steps:
        - method: click
        params: [1, 2]
        - method: click
        params:
        by: id
        value: search
      • 方式二:简化格式
        1
        2
        3
        4
        5
        6
        name: 企业微信新增通讯录成员
        deascription: 企业微信新增通讯录成员
        steps:
        - by: id
        value: search
        action: click
      • 方式三:进一步简化,如果by只是单个简单的字符串
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        name: 企业微信新增通讯录成员
        deascription: 企业微信新增通讯录成员
        steps:
        - id: click
        action: click
        - id: search
        action: sendKeys
        text: "xiaohei"
        - id: search
        sendKeys: "xiaohei"
      • 方式四:根据上一步,再次简化
        1
        2
        3
        4
        name: 企业微信新增通讯录成员
        deascription: 企业微信新增通讯录成员
        steps:
        - click: {id: search}
  3. 编写读取yaml文件的代码

    • 使用jackson读取yaml文件

    • maven pom.xml引入依赖

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
      <version>2.11.3</version>
      </dependency>

      <dependency>
      <groupId>com.fasterxml.jackson.dataformat</groupId>
      <artifactId>jackson-dataformat-yaml</artifactId>
      <version>2.11.0</version>
      </dependency>
    • 定义与yaml内容对应实体AutoModel.java

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      package com.test.framework.pojo;

      import java.util.HashMap;
      import java.util.List;

      /**
      * UI页面的基本建模
      *
      * @author jingLv
      * @date 2020/12/07
      */
      public class AutoModel {
      /**
      * 用例名称
      */
      public String name = "";
      /**
      * 用例描述
      */
      public String description = "";
      /**
      * 用例步骤
      */
      public List<HashMap<String, Object>> steps;
      }
    • 使用jackson读取方法封装

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      /**
      * 加载(yaml文件中建模的数据)
      *
      * @param path yaml文件路径
      */
      public AutoModel load(String path) {
      ObjectMapper mapper = new ObjectMapper(new YAMLFactory());
      AutoModel autoModel = null;
      try {
      autoModel = mapper.readValue(
      BasePage.class.getResource(path),
      AutoModel.class
      );
      } catch (IOException e) {
      e.printStackTrace();
      }
      return autoModel;
      }
    • 测试:

      yaml文件内容:

      1
      2
      3
      4
      5
      6
      7
      name: 百度
      description: 百度搜索
      steps:
      - action: get
      url: https://www.baidu.com/
      - click: {id: "su"}

      1
      2
      3
      4
      5
      6
      @Test
      void load() throws JsonProcessingException {
      AutoModel autoModel = basePage.load("/uiInfo/baiduUI.yaml");
      ObjectMapper mapper = new ObjectMapper();
      System.out.println(mapper.writeValueAsString(autoModel));
      }
    • 执行结果:

      image-20201208155117840

  4. 新建PO的BasePage基类

  5. 改造Web的BasePage为WebBasePage和App的BasePage为AppBasePage,并都继承BasePage的基类

  6. yaml文件生成PO模型

动态传参运行

动态传参,用例执行不依赖代码,使用Junit5提供的Launcher的方式进行命令调度传参

junit-platform-console-standalone的Jar包下载

  1. 下载jar包

    1
    wget https://repo1.maven.org/maven2/org/junit/platform/junit-platform-console-standalone/1.7.0/junit-platform-console-standalone-1.7.0.jar
  1. 指定参数运行

    1
    2
    3
    4
    5
    # 1. 测试工程maven进行打包
    mvn package -DskipTests/mvn package -Dmaven.test.skip=true

    # 2.运行Launcher
    java -jar junit-platform-console-standalone-1.7.0.jar -cp /Users/apple/JavaProject/test-framework/target/test-framework-1.0-SNAPSHOT.jar 参数

    目前使用不会,后续在补充

Lanncher编写代码执行测试用例

  1. maven工程添加依赖:

    1
    2
    3
    4
    5
    <dependency>
    <groupId>org.junit.platform</groupId>
    <artifactId>junit-platform-launcher</artifactId>
    <version>1.6.2</version>
    </dependency>
  1. 编写执行测试代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    package com.test.framework;

    import com.test.framework.testcase.WebTest;
    import org.junit.platform.launcher.Launcher;
    import org.junit.platform.launcher.LauncherDiscoveryRequest;
    import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder;
    import org.junit.platform.launcher.core.LauncherFactory;
    import org.junit.platform.launcher.listeners.SummaryGeneratingListener;
    import org.junit.platform.launcher.listeners.TestExecutionSummary;

    import static org.junit.platform.engine.discovery.ClassNameFilter.includeClassNamePatterns;
    import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
    import static org.junit.platform.engine.discovery.DiscoverySelectors.selectPackage;

    /**
    * @author jingLv
    * @date 2020/12/09
    */
    public class RunTest {

    public static void main(String[] args) {
    LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request()
    .selectors(
    selectPackage("com.test.framework"),
    // 执行的测试类
    selectClass(WebTest.class)
    )
    .filters(
    includeClassNamePatterns(".*")
    )
    .build();

    Launcher launcher = LauncherFactory.create();

    SummaryGeneratingListener listener = new SummaryGeneratingListener();
    launcher.registerTestExecutionListeners(listener);

    launcher.execute(request);

    TestExecutionSummary summary = listener.getSummary();
    System.out.println(summary.getTestsSucceededCount());
    }
    }