赞
踩
什么是
参数化
?什么又是数据驱动
?经常有人会搞不明白他们的关系,浅谈一下个人的理解,先来看两个测试中最常见的场景:
以上两种场景都有一个共同点,就是测试的执行步骤不变,变的只是输入的测试数据,那么引出两个概念——
参数化
和数据驱动
参数化:我们在写自动化用例的时候会有很多方法,一般我们都会把数据通过参数来传递给方法,而不会直接在方法中写“死”,所以方法之间的数据传递都是通过参数化来进行,利用参数化进行数据与变量的对应;比如我们的登录账号密码设置在参数中,再将参数传递到方法中。
public MainPage login(String username, String password) {
sendKeys(inputUsername,username);
sendKeys(inputPassword,password);
click(loginBtn);
return new MainPage();
}
数据驱动:将参数化中的数据来源变成从外部读取,参数有一个存放数据的地方,在用例执行的时候去去数据;这个数据存储的地方可以是我们定义的数组、hashmap,也可以是从外部文件中(excel、csv、xml、yaml等)读取。
例如上述的搜索案例,我们可以将搜索条件放入外部文件中,每次执行搜索用例时,去文件中获取数据,根据获取到的数据执行不同的搜索测试即可。
-
- 洗衣液
-
- 帽子
-
- 手套
总结下来:
数据驱动
为自动化测试框架的一种设计思想,而参数化
是实现数据驱动的一种手段,我们利用参数化来完成数据驱动,从而将数据与脚本分离
,增加了框架的可维护性
和脚本的复用性
。
PO
一起使用会“风味更佳”。我们要将数据存入文件中,不同的文件有着不同的数据格式,那么作何选择呢?
文件格式 | 优点 | 缺点 |
---|---|---|
Excel | 生成数据方便 | 二进制文件不利于版本管理 |
Csv | 可使用Excel编辑 | 文本格式方便版本管理,不容易描述复杂的层级结构 |
Yaml | 格式完备,可读性好,可以注释 | 格式简单 |
Xml | 格式完备 | 冗长复杂 |
Json | 格式完备,可读性一般 | 不能编写注释,格式死板 |
从上述对比结果中,json和yaml对于数据结构的支持和书写程度是较好的;但是,yaml的写法更简洁,并且还可以注释,因此最推荐使用的就是(从表格中的所处都位置也可猜到~)…位于C位的Yaml
!
那么到底什么是Yaml
,又如何使用,下面简单来了解一下
yaml的语法
#
表示注释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用法可参考阮一峰的博客:
https://www.ruanyifeng.com/blog/2016/07/yaml.html
既然有了数据存储的地方,那么就要对数据进行读取,这里就要介绍另一位帮手,Java的
jackson
库
jackson
是Java的一个库,用的最多的是jackson-databind
和jackson-dataformats-text
,分别用来处理json
和yaml
数据格式,它可以将文件中的数据和Java中的对象建立一种映射关系,把一个文件数据通过类型建立关联,并创建出一个类的实例,反之也可以把一个对象写入文件中。
先来看jackson-databind
对json
文件的操作
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.9.3</version>
</dependency>
public class TestFileSource {
public String name;
public int age;
}
ObjectMapper
对象,调用writeValue
对json
文件进行写操作@Test
void writeJson() throws IOException {
ObjectMapper mapper = new ObjectMapper();
TestFileSource testFileSource = new TestFileSource();
mapper.writeValue(new File("..\\demo.json"),testFileSource);
}
demo.json
文件的结果,从结果可以看到TestFileSource
类中的变量已经被写入的json
文件中{"name":null,"age":0}
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);
}
ApiDemos.testcase.TestFileSource@4562e04d
0
json
格式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);
}
{
"name" : null,
"age" : 0
}
jackson-databind
GitHub地址:https://github.com/FasterXML/jackson-databind再来看jackson-dataformats-text
,这是一个可以对YAML
、CSV
、Properties
和XML
文件进行操作的库,也是目前最常用的,不过这里我们只重点关注其对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地址:https://github.com/FasterXML/jackson-dataformats-text
上面提到了数据驱动可以在几个方面进行:
- 测试数据的数据驱动
- 测试步骤的数据驱动
- 定位符
- 行为流
- 断言的数据驱动
下面分别来看如何进行数据驱动
说到测试数据的数据驱动,就必然离不开测试框架的参数化,毕竟测试数据是传给用例的,用例是由框架来管理的,这里以目前最推荐使用的
Junit5
框架为例,介绍参数化的使用
在Junit5中,提供了@ParameterizedTest
注解来实现方法的参数化设置,另外@ValueSource
注解用来存放数据,写法如下:
@ParameterizedTest
@ValueSource(strings = { "racecar", "radar", "able was I ere I saw elba" })
void palindromes(String candidate) {
assertTrue(StringUtils.isPalindrome(candidate));
}
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));
}
最终,Junit5提供了@CsvFileSourc
注解来实现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
文件完成测试数据驱动
@MethodSource
,此注解提供的方法是我们做测试数据驱动的核心,它可以让方法接收指定方法的返回值作为参数化的入参,用法是在注解的括号中填入数据来源的方法名,具体用法如下:@ParameterizedTest
@MethodSource("stringProvider")
void testWithExplicitLocalMethodSource(String argument) {
assertNotNull(argument);
}
static Stream<String> stringProvider() {
return Stream.of("apple", "banana");
}
有了@MethodSource
的参数化支持,我们就可以在方法中利用jackson
库对yaml
文件进行数据读取,从而完成数据驱动了
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)); }
对于测试步骤的数据驱动主要针对两点:
好比下面这样的,以雪球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等数据文件,会造成用例可读性变差,维护复杂度变高;
首先来考虑我们的剥离到yaml
中的数据结构
search:
steps:
- id: search_input_text
send: pdd
- id: name
cancel:
steps:
- id: action_close
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文件中的结构保持一致: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<>();
}
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<>();
}
yaml
数据的方法,完成PO
中行为流的封装;
jackson
来解析yaml
数据,我们需要文件的地址
,另外我们还需要知道当前执行的方法
,用来去yaml
中取方法对应的定位符
和行为流
,所以初步设想应该有method
和path
两个参数: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(); //默认操作是点击 } }); }
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();
}
}
到这里,测试步骤的数据驱动算是完成了一个基本模板,还有很多可以优化的地方,比如上面的SearchPage
的PO
中,parseSteps
的两个参数method
和path
都是有规律可循的:
method
和当前执行的方法名是定义好保持一致的PO
所对应的yaml
文件的path
是固定的下面针对这个点做个小优化
这里将会对上一节中的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
目录下即可
method
和path
参数的问题都解决了,在来看现在的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();
}
}
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);
}
在文章前面提到过获取元素属性,在自动化测试过程中,经常要获取元素属性来作为方法的返回值,以供我们进行其他操作或断言,其中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"));
}
接着我们到SearchPage
的PO
中实现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)消费者
根据自己的需求
去中间商
那里拿到商品
,从result
中get
到price
的值
public Double getCurrentPrice(){
parseSteps();
return Double.valueOf(getResult().get("price").toString());
}
这样就成功完成了这个交易场景的闭环,股票价格price
被成功返回至用例中
有了上面的铺垫,断言的数据驱动就显得简单多了,我个人有时候也简单的把它归为测试数据的驱动中
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)); }
注:其实这里应该说还是测试数据驱动,并不能算是断言的驱动,真正想做成断言的驱动还需要封装类似测试步骤驱动的形式,目前我没有做这层封装,因为在使用中发现断言的类型很多,直接在用例里面写也很方便易读,加上目前时间精力也有限,待后续需要的时候再继续补充~
说的再多,不如实际跑一下,检验一下框架封装后的实际运行效果
折腾了这么久,总算是“大功告成”了,之所以加个引号,是因为这个仅仅是个开始,只能算是初具雏形,像文章中提到的被测系统切换、版本切换、多元素查找等都还未实现,后续会持续学习更新;
我本人也是在学习踩坑中,本文主要问自己的学习笔记总结,经验少,能力弱,基础差,可能有很多错误或表述不恰当的地方;倘若有幸被哪位读者大佬看到,希望多多包涵,也请不要吝啬您的指教,欢迎指出,我会虚心讨教,谢谢~
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。