参数化和数据驱动在App自动化测试中的应用(基于Junit5和YAML)-01

1,043 阅读21分钟

1、何为数据驱动

什么是参数化?什么又是数据驱动?经常有人会搞不明白他们的关系,浅谈一下个人的理解,先来看两个测试中最常见的场景:

  • 登录:不同的用户名,不同的密码,不同的组合都需要做登录场景的测试,正常的排列组合下可能会产生多个用例
  • 搜索:不同的搜索条件产生不同的搜索结果,搜索也是常见的测试项,单个搜索参数或者多种搜索参数的组合;同样也会产生多个用例。

以上两种场景都有一个共同点,就是测试的执行步骤不变,变的只是输入的测试数据,那么引出两个概念——参数化数据驱动

  • 参数化:我们在写自动化用例的时候会有很多方法,一般我们都会把数据通过参数来传递给方法,而不会直接在方法中写“死”,所以方法之间的数据传递都是通过参数化来进行,利用参数化进行数据与变量的对应;比如我们的登录账号密码设置在参数中,再将参数传递到方法中。

    public MainPage login(String username, String password) {
            sendKeys(inputUsername,username);
            sendKeys(inputPassword,password);
            click(loginBtn);
            return new MainPage();
    }
    
  • 数据驱动:将参数化中的数据来源变成从外部读取,参数有一个存放数据的地方,在用例执行的时候去去数据;这个数据存储的地方可以是我们定义的数组、hashmap,也可以是从外部文件中(excel、csv、xml、yaml等)读取。 例如上述的搜索案例,我们可以将搜索条件放入外部文件中,每次执行搜索用例时,去文件中获取数据,根据获取到的数据执行不同的搜索测试即可。

    -
     - 洗衣液
    -
     - 帽子
    -
     - 手套
    

总结下来

数据驱动为自动化测试框架的一种设计思想,而参数化是实现数据驱动的一种手段,我们利用参数化来完成数据驱动,从而将数据与脚本分离,增加了框架的可维护性和脚本的复用性

2、为什么要做数据驱动

测试数据
  • 在执行测试工作过程中,有很多过程是需要动态变化的,如果每一次的变化都需要编码部署,那么整个执行的流程就会边长;
  • 对于业务测试工程师来说,维护自动化代码有一定的门槛,需要熟悉编程语言和测试框架的结构;
  • 定义好了数据驱动,将变化的数据放入配置文件中进行维护,既便捷(无需找到对应代码修改部署),也降低了维护的门槛(业务测试只需要在配置文件中修改数据即可)
测试步骤
  • 与测试数据的数据驱动大致相同,主要也是方便业务测试维护,降低维护门槛和代码修改部署出错的风险;修改配置文件,整个业务行为和抽象是不用改变的,当然,在UI自动化中配合PO一起使用会“风味更佳”。
动态生成测试步骤
  • 手工录制测试步骤,直接生成代码比较困难,可以生成步骤的配置文件,让代码去读配置文件,完成自动化的回放;(此方面本人暂时仅了解过,还未实践落地,理论上是可以实现的。)

3、在哪里做数据驱动

不要在哪里做数据驱动
  • 不要在测试用例内完成大量的数据驱动: 用例通过PO的调用是能够非常清晰展现出业务执行场景的,业务才是用例的核心;一旦在用例里使用了大量数据驱动,如调用各种yaml、csv等数据文件,会造成用例可读性变差,维护复杂度变高;
可以在哪里做数据驱动
  • 测试数据的数据驱动
  • 测试步骤的数据驱动
    • 定位符
    • 行为流
  • 断言的数据驱动

4、如何做数据驱动

4.1 数据格式的选择

我们要将数据存入文件中,不同的文件有着不同的数据格式,那么作何选择呢?

  • 不同数据格式文件的对比
文件格式 优点 缺点
Excel 生成数据方便 二进制文件不利于版本管理
Csv 可使用Excel编辑 文本格式方便版本管理,不容易描述复杂的层级结构
Yaml 格式完备,可读性好,可以注释 格式简单
Xml 格式完备 冗长复杂
Json 格式完备,可读性一般 不能编写注释,格式死板

从上述对比结果中,json和yaml对于数据结构的支持和书写程度是较好的;但是,yaml的写法更简洁,并且还可以注释,因此最推荐使用的就是(从表格中的所处都位置也可猜到~)...位于C位的Yaml! 那么到底什么是Yaml,又如何使用,下面简单来了解一下

4.2 Yaml文件的使用

yaml的语法

  • 大小写敏感
  • 使用缩进表示层级关系
  • 缩进时不允许使用Tab键,只允许使用空格。
  • 缩进的空格数目不重要,只要相同层级的元素左侧对齐即可
  • # 表示注释

yaml支持的三种数据结构

  • 纯量(scalars):单个的、不可再分的值,例如数字、字符串、布尔值等

  • 对象:键值对的集合,又称为映射(mapping)/ 哈希(hashes) / 字典(dictionary)

    #键值对形式
    key: value
    #行内对象
    person: { name: allen, age: 25 }
    
  • 数组:一组按次序排列的值,又称为序列(sequence) / 列表(list)

    #以-开头表示为一个数组里的值
    - A
    - B
    - C
    #数组内嵌套子数组,用一个空格缩进表示
    - 
     - a
     - aa
    - 
     - b
     - bb
    
  • 对象和数组可以结合使用,形成复合结构

    languages:
     - Ruby
     - Perl
     - Python 
    websites:
     YAML: yaml.org 
     Ruby: ruby-lang.org 
     Python: python.org 
     Perl: use.perl.org 
    

这里举的是最常用的用法,更多的yaml用法可参考阮一峰的博客: www.ruanyifeng.com/blog/2016/0…

4.3 数据读取-jackson

既然有了数据存储的地方,那么就要对数据进行读取,这里就要介绍另一位帮手,Java的jackson

jackson是Java的一个库,用的最多的是jackson-databindjackson-dataformats-text,分别用来处理jsonyaml数据格式,它可以将文件中的数据和Java中的对象建立一种映射关系,把一个文件数据通过类型建立关联,并创建出一个类的实例,反之也可以把一个对象写入文件中。

4.3.1 jackson-databind

先来看jackson-databindjson文件的操作

  • 添加maven依赖
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.9.9.3</version>
    </dependency>
    
  • 写json文件
    1)先创建一个类,包含变量name,age
    public class TestFileSource {
        public String name;
        public int age;
        }
    
    2)创建单元测试,创建ObjectMapper对象,调用writeValuejson文件进行写操作
    @Test
    void writeJson() throws IOException {
        ObjectMapper mapper = new ObjectMapper();
        TestFileSource testFileSource = new TestFileSource();
        mapper.writeValue(new File("..\\demo.json"),testFileSource);
    }
    
    3)得到demo.json文件的结果,从结果可以看到TestFileSource类中的变量已经被写入的json文件中
    {"name":null,"age":0}
    
  • 读json文件
    1)创建单元测试,创建ObjectMapper对象,调用readValue方法对json文件进行数据读取
    @Test
    void readJson() throws IOException {
        ObjectMapper mapper = new ObjectMapper();
        TestFileSource testFileSource = mapper.readValue(TestFileSource.class.getResourceAsStream("/demo.json"), TestFileSource.class);
        System.out.println(testFileSource);
        System.out.println(testFileSource.age);
    }
    
    2)读取结果
    	ApiDemos.testcase.TestFileSource@4562e04d
    	0
    
  • 输出漂亮的json格式
    1)创建单元测试,创建ObjectMapper对象,调用writerWithDefaultPrettyPrinter().writeValueAsString方法可对指定对象进行json数据格式的输出
    @Test
    void prettyPrintJson() throws JsonProcessingException {
        ObjectMapper mapper = new ObjectMapper();
        // pretty print
        String json = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(new TestFileSource());
        System.out.println(json);
    }
    
    2)打印结果
    {
      "name" : null,
      "age" : 0
    }
    
  • 参考链接 jackson-databindGitHub地址:github.com/FasterXML/j…
4.3.2 jackson-dataformats-text

再来看jackson-dataformats-text,这是一个可以对YAMLCSVPropertiesXML文件进行操作的库,也是目前最常用的,不过这里我们只重点关注其对YAML文件的操作

  • 添加maven依赖

    <dependency>
        <groupId>com.fasterxml.jackson.dataformat</groupId>
        <artifactId>jackson-dataformat-yaml</artifactId>
        <version>2.9.8</version>
    </dependency>
    
  • 读yaml文件 想要读取yaml文件,最主要的是在new ObjectMapper对象的时候加入new YAMLFactory(),这样就成功切换至yaml操作的状态,然后利用readValue方法就可以完成对yaml文件的数据读取了
    1)创建yaml文件

    name: allen
    age: 11
    

    2)创建ObjectMapper对象,设置new YAMLFactory()

    @Test
    void readYaml() throws IOException {
            ObjectMapper mapper = new ObjectMapper(new YAMLFactory());
            TestFileSource testFileSource = mapper.readValue(TestFileSource.class.getResourceAsStream("/demo2.yaml"), TestFileSource.class);
            System.out.println(testFileSource);
            System.out.println(testFileSource.age);
    }
    
  • 打印结果

    ApiDemos.testcase.TestFileSource@ba2f4ec
    11
    

    readValue的方法中可以看到,第一个参数填的是文件地址,第二个参数就是精髓所在!我们可以给定一个对象类型,或者一个二维数组等,用来产生映射关系,将文件数据和我们的对象绑定,方便数据的读取。 如上述例子中我们通过TestFileSource的实例化对象来调用age变量,

  • 输出漂亮的yaml格式
    与json输出的方式基本一致,只需要在new ObjectMapper对象的时候加入new YAMLFactory()即可 1)创建类和类的成员变量,包含纯量、数组和哈希

    public class TestFileSource {
    
        public String name = "tester";
        public int age = 2;
        public Object[][] arr= {{1,2,3,},{"a","b","c"}};
        public HashMap<String,Object> map = new HashMap<String, Object>(){
            {
            put("name","tester");
            put("sex","男");
            }
        };
    }
    

    2)创建单元测试,创建ObjectMapper对象,加入new YAMLFactory() 参数,调用writerWithDefaultPrettyPrinter().writeValueAsString方法可对指定对象进行yaml数据格式的输出

    @Test
    void prettyPrintYaml() throws JsonProcessingException {
        ObjectMapper mapper = new ObjectMapper(new YAMLFactory());
        // pretty print
        String json = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(new TestFileSource());
        System.out.println(json);
    }
    

    3)打印结果

    ---
    name: "tester"
    age: 2
    arr:
    - - 1
      - 2
      - 3
    - - "a"
      - "b"
      - "c"
    map:
      sex: "男"
      name: "tester"
    
  • 参考链接 jackson-dataformats-text GitHub地址:github.com/FasterXML/j…

5、数据驱动

上面提到了数据驱动可以在几个方面进行:

  • 测试数据的数据驱动
  • 测试步骤的数据驱动
    • 定位符
    • 行为流
  • 断言的数据驱动

下面分别来看如何进行数据驱动

5.1 测试数据的数据驱动

5.1.1 Junit5的参数化

说到测试数据的数据驱动,就必然离不开测试框架的参数化,毕竟测试数据是传给用例的,用例是由框架来管理的,这里以目前最推荐使用的Junit5框架为例,介绍参数化的使用

@ParameterizedTest+@ValueSource参数化

在Junit5中,提供了@ParameterizedTest注解来实现方法的参数化设置,另外@ValueSource注解用来存放数据,写法如下:

@ParameterizedTest
@ValueSource(strings = { "racecar", "radar", "able was I ere I saw elba" })
void palindromes(String candidate) {
    assertTrue(StringUtils.isPalindrome(candidate));
}
@ParameterizedTest+@CsvSource参数化

Junit5还提供了@CsvSource注解来实现csv格式的参数传递,写法如下:

@ParameterizedTest
@CsvSource({
        "滴滴,滴滴出行",
        "alibaba,阿里巴巴",
        "sougou,搜狗"
})
public void searchStocks(String searchInfo,String exceptName)   {
    String name = searchpage.inputSearchInfo(searchInfo).getAll().get(0);
    assertThat(name,equalTo(exceptName));
}
@ParameterizedTest+@CsvFileSourc数据驱动

最终,Junit5提供了@CsvFileSourc注解来实现csv数据格式的数据驱动,可以传递csv文件路径来读取数据,写法如下:

  • csv数据文件:
    pdd
    xiaomi
    pdd
    
  • 用例实现:
    @ParameterizedTest
    @CsvFileSource(resources = "/data/SearchTest.csv")
    void choose(String keyword){
    ArrayList<String> arrayList = searchPage.inputSearchInfo(keyword).addSelected();
    }
    

对于简单的数据结构,可以使用CSV,上面也说过,较为复杂的数据结构,推荐使用yaml,接下来看如何用yaml文件完成测试数据驱动

@ParameterizedTest+@MethodSource参数化
  • 先来看Junit5提供的另一个注解——@MethodSource,此注解提供的方法是我们做测试数据驱动的核心,它可以让方法接收指定方法的返回值作为参数化的入参,用法是在注解的括号中填入数据来源的方法名,具体用法如下:
    @ParameterizedTest
    @MethodSource("stringProvider")
    void testWithExplicitLocalMethodSource(String argument) {
        assertNotNull(argument);
    }
    
    static Stream<String> stringProvider() {
        return Stream.of("apple", "banana");
    }
    
@ParameterizedTest+@MethodSource参数化 + jackson yaml数据驱动

有了@MethodSource的参数化支持,我们就可以在方法中利用jackson库对yaml文件进行数据读取,从而完成数据驱动了

  • 现有如下yaml数据文件,我需要取出testdata中的测试数据
    config:
      username: 888
      password: 666
      testdata:
        滴滴: 滴滴出行
        alibaba: 阿里巴巴
        sougou: 搜狗
    
  • 创建Config类:
    import java.util.HashMap;
    
    public class Config {
        public String username;
        public String password;
        public HashMap<String,String> testdata = new HashMap<>();
    }
    
  • 创建Config对象,与yaml文件建立映射关系,读取数据,通过@MethodSource完成数据的参数化传递
    public class TestSteps {
    
        @ParameterizedTest
        @MethodSource("YamlData")
        public void search(String searchInfo,String exceptName)   {
            String name = searchpage.inputSearchInfo(searchInfo).getAll().get(0);
            assertThat(name,equalTo(exceptName));
        }
    
        static Stream<Arguments> YamlData() throws IOException {
        ObjectMapper mapper = new ObjectMapper(new YAMLFactory());
        Config data = mapper.readValue(Config.class.getResourceAsStream("/demo2.yaml"), Config.class);
        List<Arguments> list = new ArrayList<>();
        Arguments arguments = null;
        for (String key : data.testdata.keySet()) {
            Object value = data.testdata.get(key);
            arguments = arguments(key, value);
            list.add(arguments);
        }
        return Stream.of(list.get(0),list.get(1),list.get(2));
       }
    
    为了保证运行通过,可以先简单打印验证一下:
    在这里插入图片描述

5.2 测试步骤的数据驱动

对于测试步骤的数据驱动主要针对两点:

  • 定位符: 我们做App自动化的时候可以把定位符合定位器直接写在PO中,也可以将其剥离出来,写在类似yaml的文件中,定义好格式个对象的映射关系即可完成定位符的数据驱动。
  • 行为流: 与定位符的剥离思想一致,行为流原本也是写在PO中的各个方法,这些行为流和定位符是紧密关联的,因此也可以剥离出来,和定位符在一起组成测试步骤的数据驱动。

好比下面这样的,以雪球App的搜索场景为例:

public class SearchPage extends BasePage{
	//定位符
    private By inputBox = By.id("search_input_text");
    private By clickStock = By.id("name");
    private By cancel = By.id("action_close");
	//行为流
	//搜索股票
    public SearchPage search(String sendText){ 
        sendKeys(inputBox,sendText);
        click(clickStock); 
        return this;
    }
	//取消返回
	public App cancel(){
       click(cancel);
       return new App();
    }
}

:测试步骤的数据驱动是指把PO中变化的量剥离出来,不是对用例里的调用步骤进行封装 在上面已经提到过不要在测试用例内完成大量的数据驱动: 用例通过PO的调用是能够非常清晰展现出业务执行场景的,业务才是用例的核心;一旦在用例里使用了大量数据驱动,如调用各种yaml、csv等数据文件,会造成用例可读性变差,维护复杂度变高;

5.2.1 设计思路

首先来考虑我们的剥离到yaml中的数据结构

  • 做测试步骤的数据局驱动我们希望可以将一个用例中的步骤方法清晰的展示出来,在对应的方法中包括了方法对应的定位符和行为流,这样能和PO中的结构保持一致,更易读易维护;如下:
    search:
        steps:
          - id: search_input_text
            send: pdd
          - id: name
    cancel:
        steps:
          - id: action_close
    
  • 另外我们还要考虑扩展性,之前提到了还有测试断言的数据驱动,另外还有一点没提到的是,框架的健壮程度还要考虑被测系统(Android,IOS)的通用性、版本变更、元素定位符的多样性等;这样考虑的话就应该有多个分类,一个分类中包含了PO中的所有方法,一个分类中包含了版本、系统等信息等,如下(SearchPage.yaml):
    #方法
    methods:
      search:
        steps:
          - id: search_input_text
            send: pdd
          - id: name
      cancel:
    	steps:
    	  - id: action_close
    
    #定位符对应系统、版本信息
    elements:
      search_input_text:
        element:
        ...
    
    #断言
    asserts:
      search:
      	assert:
      	...
      cancel:
        assert:
        ...
    
  • 按照上述的思路,以搜索步骤为例,我们需要一个Model类,用来映射不同的数据模块(方法、版本、断言),对不同的模块需要一一对应的类,类的成员变量结构与yaml文件中的结构保持一致:
    1)创建PageObjectModel
    import java.util.HashMap;
    public class PageObjectModel {
    	public HashMap<String, PageObjectMethod> methods = new HashMap<>();
        public HashMap<String, PageObjectElement> elements = new HashMap<>();
    	public HashMap<String, PageObjectAssert> asserts = new HashMap<>();
    }
    
    2)创建对应数据模块的类PageObjectMethod
    public class PageObjectMethod {
        public List<HashMap<String, String>> getSteps() {
            return steps;
        }
    
        public void setSteps(List<HashMap<String, String>> steps) {
            this.steps = steps;
        }
    
        public List<HashMap<String,String>> steps = new ArrayList<>();
    }
    
    3)实现解析yaml数据的方法,完成PO中行为流的封装;
    • 首先按照之前介绍过的通过jackson来解析yaml数据,我们需要文件的地址,另外我们还需要知道当前执行的方法,用来去yaml中取方法对应的定位符行为流,所以初步设想应该有methodpath两个参数:
    public void parseSteps(String method,String path){
            ObjectMapper mapper = new ObjectMapper(new YAMLFactory());
            try {
                PageObjectModel model = mapper.readValue(BasePage.class.getResourceAsStream(path),PageObjectModel.class);
                parseStepsFromYaml(model.methods.get(method));
            }catch (IOException e) {
                e.printStackTrace();
            }
        }
    
    • 上面的方法中可以看到调用了一个parseStepsFromYaml方法,这个方法是将从yaml中获取到的数据进行处理,拿到对应方法定位符再拿到定位符紧跟的行为流完成对应的操作步骤(点击、输入、获取属性等);之所以将这个方法单独抽离出来,是因为后面会对parseSteps重载,方便复用,后面会介绍到。 如下:我们要通过methods里的search方法拿到对应的步骤steps里的id,在根据id下的send值进行输入操作
    methods:
     search:
      steps:
       - id: search_input_text
         send: pdd
      - id: name
    
    private void parseStepsFromYaml(PageObjectMethod steps){ //获取方法名method
            steps.getSteps().forEach(step ->{
                WebElement element = null;
                if (step.get("id") != null){
                    element = findElement(By.id(id));
                }else if (step.get("xpath") != null){
                    element = findElement(By.id(step.get("xpath")));
                }else if (step.get("aid") != null){
                    element = findElement(MobileBy.AccessibilityId(step.get("aid")));
                if (step.get("send") != null){
                    element.sendKeys(step.get("send"));
                }else if (step.get("get") != null){
                      findElement(by).getAttribute(get);
                }
                else {
                    element.click();  //默认操作是点击
                }
            });
        }
    
    4)这个时候再回到我们的PO里,就变成了这个样子,看一下PO是不是一下子变得简洁了许多:
    public class SearchPage extends BasePage{
    	//行为流
    	//搜索股票
        public SearchPage search(String sendText){ 
            parseSteps("search","/com.xueqiu.app/page/SearchPage.yaml");
            return this;
        }
    	//取消返回
    	public App cancel(){
           parseSteps("cancel","/com.xueqiu.app/page/SearchPage.yaml");
           return new App();
        }
    }
    

到这里,测试步骤的数据驱动算是完成了一个基本模板,还有很多可以优化的地方,比如上面的SearchPagePO中,parseSteps的两个参数methodpath都是有规律可循的:

  • method和当前执行的方法名是定义好保持一致的
  • 当前PO所对应的yaml文件的path是固定的

下面针对这个点做个小优化

5.2.2 框架优化

这里将会对上一节中的parseSteps方法进行优化,减少重复性工作

  • 先来解决方法名method的问题,来看Thread的一个方法:Thread.currentThread().getStackTrace() 利用这个方法可以打印出当前方法执行的全部过程,写单测来验证,将每一步的方法名都打印出来:

    void testMethod(){
            Arrays.stream(Thread.currentThread().getStackTrace()).forEach(stack ->{
                System.out.println(stack.getMethodName());
            });
            System.out.println("当前调用我的方法是:"+Thread.currentThread().getStackTrace()[2].getMethodName());
        }
    
    @Test
    void getMethodName(){
        testMethod();
        }
    

    执行结果:

    getStackTrace
    testMethod   //当前执行的方法
    getMethodName  //调用testMethod的方法
    invoke0
    invoke
    invoke
    invoke
    invokeMethod
    proceed
    //...这里省略中间很多不重要的部分
    execute
    execute
    startRunnerWithArgs
    startRunnerWithArgs
    prepareStreamsAndStart
    main
    当前执行的方法是:getMethodName
    

    从结果中可以看到,当方法被调用时,调用它的方法名会在输出结果的索引2位置,因此通过此方法就可以成功的拿到我们所需的method参数

  • 再来解决yaml文件路径的path参数,这里可以借助java.lang.Class.getCanonicalName()方法,此方法可以返回当前类名,包括类所在的包名,如下:

    @Test
    void getPath(){
        System.out.println(this.getClass().getCanonicalName());
    }
    
    //打印结果
    com.xueqiu.app.testcase.TestSteps
    
  • 稍加改造就可以变成地址信息:

    @Test
    void getPath(){
        System.out.println(this.getClass().getCanonicalName());
        String path = "/com.xueqiu.app" + this.getClass().getCanonicalName().split("app")[1].replace(".", "/") + ".yaml";
        System.out.println(path);
    }
    

    打印结果:

    com.xueqiu.app.testcase.TestSteps
    /com.xueqiu.app/testcase/TestSteps.yaml
    

    这样我们就将当前类的信息转变成了一个地址信息,后面我们只需要将对应的yaml文件以和类相同的命名相同路径结构存放在resources目录下即可

    • 现在methodpath参数的问题都解决了,在来看现在的parseSteps方法:
    //解析步骤
    public void parseSteps(String method) {
        ObjectMapper mapper = new ObjectMapper(new YAMLFactory());
        String path = "/com.xueqiu.app" + this.getClass().getCanonicalName().split("app")[1].replace(".", "/") + ".yaml";
        try {
            PageObjectModel model = mapper.readValue(this.getClass().getResourceAsStream(path),PageObjectModel.class);
            parseStepsFromYaml(model.methods.get(method));
        }catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    public void parseSteps(){
        String method = Thread.currentThread().getStackTrace()[2].getMethodName();
        parseSteps(method);
    }
    
    • 此时再次回到SearchPage的PO中,可以看到更加的简洁了,甚至变成了“傻瓜操作”:
    public class SearchPage extends BasePage{
    
        public SearchPage search(){ 
            parseSteps(); 
            return this;
        }
    
        public App cancel(){
            parseSteps();
            return new App();
        }
    }
    
    send参数化处理
    • 看似好像大功告成,又出现了新的问题,不知道大家注意到没有,search方法其实是需要send值的,而现在我们的send值是写死在yaml中的,这反而违背了我们参数化和数据驱动的原则:
    methods:
      search:
        steps:
          - id: search_input_text
            send: pdd  #send的内容被写死在了这里
          - id: name
    
    • 所以我们需要继续解决这个问题,将send的值进行参数化

    1) 既然是参数化,那就要把send的值变成参数,这里用$sendText来表示是参数

    methods:
      search:
        steps:
          - id: search_input_text
      #      send: pdd
            send: $sendText #表示参数化
          - id: name
    

    2)在search方法中使用HashMap将用例传递过来的测试数据保存至其中,用来传递到parseSteps步骤解析方法中。

    public SearchPage search(String sendText){ 
        HashMap<String,Object> map = new HashMap<>();
        map.put("sendText",sendText);
        setParams(map);
        parseSteps(); 
        return this;
    }
    

    3)再在parseSteps方法所处的类中添加HashMap类型的params变量,用来接收PO传过来的sendText测试数据

    private static HashMap<String,Object> params = new HashMap<>();
    
    public HashMap<String, Object> getParams() {
        return params;
    }
    //测试步骤参数化
    public void setParams(HashMap<String, Object> params) {
        this.params = params;
    }
    

    4)最后修改parseStepsFromYaml方法中的send值获取方式,将占位的参数$sendText替换成实际传递过来的测试数据sendText

    if (step.get("send") != null){
            String send = step.get("send").replace("$sendText",params.get("sendText").toString());
            element.sendKeys(send);
    }
    
getAttribute实现

在文章前面提到过获取元素属性,在自动化测试过程中,经常要获取元素属性来作为方法的返回值,以供我们进行其他操作或断言,其中text是我们最常获取的属性,下面来实现此方法的数据驱动

在上面的搜索股票场景下,加上一步获取股票的价格信息

  • 先看一下思路,按照之前的设计,在yaml中的定位符后面跟着的就是行为流,假定有一个getCurrentPrice方法,通过get text来获取text属性,写法如下:

    getCurrentPrice:
        steps:
          - id: current_price
            get: text
    
  • 这个时候就可以在parseStepsFromYaml方法中加入属性获取的解析逻辑,通过get来传递要获取的属性

    if (step.get("send") != null){
            String send = step.get("send").replace("$sendText",params.get("sendText").toString());
            element.sendKeys(send);
    }else if (step.get("get") != null){
            String attribute = element.getAttribute(step.get("get"));
                    }
    
  • 接着我们到SearchPagePO中实现getCurrentPrice方法,这个时候就会发现一个问题:

    public Double getCurrentPrice(){
            parseSteps();
           // return ???;
           }
    

    没错,text属性获取到了,可以没有回传出来,getCurrentPrice方法没有return值;我们要将parseStepsFromYaml获取到的属性值通过一个“中间商"给传递到getCurrentPrice方法中,然后再return到用例中供我们断言使用

    • 语言描述比较晦涩,下面我以一个市场供需买卖的场景来说明整个设计流程:
      在这里插入图片描述

    1)产生市场需求yaml中定义好数据结构

    methods:
      search:
        steps:
          - id: search_input_text
            send: $sendText
          - id: name
    
      getCurrentPrice:
        steps:
          - id: current_price
            get: text
            dump: price
    
      cancel:
        steps:
          - id: action_close
    

    2) 实现“中间商”,这个“中间商”就是一个HashMap,将它取名为result

    private static HashMap<String,Object> result = new HashMap<>();
    
    //测试步骤结果读取
    public static HashMap<String, Object> getResult() {
        return result;
    }
    

    3)供应商根据市场需求产生产品并提供给中间商,获取属性并将属性值存入result

    if (step.get("send") != null){
            String send = step.get("send").replace("$sendText",params.get("sendText").toString());
            element.sendKeys(send);
    }else if (step.get("get") != null){
            String attribute = element.getAttribute(step.get("get"));
            result.put(step.get("dump"),attribute);
                    }
    

    4)消费者根据自己的需求中间商那里拿到商品,从resultgetprice的值

    public Double getCurrentPrice(){
        parseSteps();
        return Double.valueOf(getResult().get("price").toString());
    }
    

    这样就成功完成了这个交易场景的闭环,股票价格price被成功返回至用例中

5.3 断言的数据驱动

有了上面的铺垫,断言的数据驱动就显得简单多了,我个人有时候也简单的把它归为测试数据的驱动中

  • 因为每个测试数据在传入用例跑完后,都会对应有断言来进行结构判定,因此将测试数据对应的断言数据在一个yaml文件中,写入一个数组里,再同测试数据一起获取传入到用例中
    -
     - didi
     - 100d
    -
     - alibaba
     - 120d
    -
     - sougou
     - 80d
    
    • 回到最初的测试数据数据驱动,把数据获取传入
    @ParameterizedTest
    @MethodSource("searchYamlData")
    void search(String searchInfo,String exceptPrice ){
        Double currentPrice = searchPage.search(searchInfo).getCurrentPrice();
        assertThat(currentPrice,greaterThanOrEqualTo(Double.parseDouble(exceptPrice)));
    }
    
    static Stream<Arguments> searchYamlData() throws IOException {
        Arguments arguments = null;
        List<Arguments> list = new ArrayList<>();
        ObjectMapper mapper = new ObjectMapper(new YAMLFactory());
    
        String path1 = "/com.xueqiu.app" + TestSearch.class.getCanonicalName().split("app")[1].replace(".","/") + ".yaml";
        Object[][] searchData = mapper.readValue(TestSearch.class.getResourceAsStream(path1), Object[][].class);
        for (Object[] entrySet : Arrays.asList(searchData)){
            String key = Arrays.asList(entrySet).get(0).toString();
            String value =  Arrays.asList(entrySet).get(1).toString();
            arguments = arguments(key,value);
            list.add(arguments);
        }
        return Stream.of(list.get(0),list.get(1),list.get(2));
    }
    
    注:其实这里应该说还是测试数据驱动,并不能算是断言的驱动,真正想做成断言的驱动还需要封装类似测试步骤驱动的形式,目前我没有做这层封装,因为在使用中发现断言的类型很多,直接在用例里面写也很方便易读,加上目前时间精力也有限,待后续需要的时候再继续补充~

6、运行效果

说的再多,不如实际跑一下,检验一下框架封装后的实际运行效果

在这里插入图片描述

  • 用例运行结果:
    在这里插入图片描述

7、写在最后

折腾了这么久,总算是“大功告成”了,之所以加个引号,是因为这个仅仅是个开始,只能算是初具雏形,像文章中提到的被测系统切换、版本切换、多元素查找等都还未实现,后续会持续学习更新; 我本人也是在学习踩坑中,本文主要问自己的学习笔记总结,经验少,能力弱,基础差,可能有很多错误或表述不恰当的地方;倘若有幸被哪位读者大佬看到,希望多多包涵,也请不要吝啬您的指教,欢迎指出,我会虚心讨教,谢谢~