其他测试框架 测试开发之路 ---- 可读性,可维护性,可扩展性

孙高飞 · 2016年05月11日 · 最后由 孙高飞 回复于 2017年03月30日 · 1425 次阅读
本帖已被设为精华帖!

第一部分:测试开发之路 ---- 框架中数据的管理策略
第二部分:测试开发之路 ---- 数据驱动及其变种
第三部分:测试开发之路 ---- 可读性,可维护性,可扩展性
第四部分:测试开发之路 ---- 可读性,可维护性,可扩展性 (续)

前言

好久没更新了,最近家里出了点事一直没顾上. 本来今天这个话题之前都写好草稿了.但因为家里的事, 我一直都没心情完善它. 今天我们讨论一下吧. 之所以把可读性,可维护性,可扩展性放到一块说是因为解决它们的思路往往是很像的. 你做好了一些事情,以上三个问题也就都解决了,例如可读性好了,可维护性也就差不了. 所以接下来我可能在文章中穿插着讲这三个问题,并不是按绝对的先后顺序来说. 同时把这个话题提前到第三章来说, 也是因为我觉得这是一个大家非常容易忽略,但是在 case 达到一定量级的时候会出现很大的问题的一个点,也许大家觉得我讲这些有的没的根本没用,我们以前没考虑这些的时候也不是照样干活么.那么在下面的文章里,我会跟大家解释,其实这很重要. 我不会讲什么让你们设计高内聚,低耦合 的软件这种废话.我们来实际举一些例子来看看我们一般是怎么做的.

测试框架与测试脚本的目标(部分)

  1. Tests as Documentation(你能很容易通过测试脚本理解被测软件的功能) ----可读性
  2. Defect Localization(通过测试脚本能够快速定位 bug 的位置) ----可读性与隔离性
  3. Tests should be easy to write and maintain(测试脚本应该是容易编写和维护的) ----可维护性
  4. Tests should be easy to improve when product changes(当产品变化时,测试应该是很容易扩展自身以适应变化的)----可扩展性

分层

为了提高我们测试脚本的质量, 分层显然是最常用的方法. 想象一下如果我们把根测试所有相关的东西都放在脚本里那是怎样的一种灾难,每次你去看脚本的时候都会一个头两个大。其一你不知道脚本在干嘛,其二你根本不敢随便动这个脚本。深怕动了哪里就破坏了这条脚本。所以当我们作了分层后,将责任划分出去,分而治之,每一层负责特定的功能,其他层不用担心这些特定的功能。
原则:
测试脚本只关注被测的功能逻辑,其他一切责任分层出去,或交给框架作,或交给其他模块作。
常用的分层方式:

  1. 数据驱动,具体数据驱动的实现请看:数据驱动及其变种。把测试参数的构建分离出去,减少脚本复杂度
  2. 注册式数据管理,具体实现请看:测试数据管理策略. 我们把测试数据的构建与销毁分离出去,减少脚本复杂度
  3. page object,UI 自动化常用的模式. 我看到的大家常用的方式就是把页面元素的定义分到单独的类中.下面来看看我曾经怎么做这个分层的.

脚本是这么写的:

driver.page("登陆页面").sendKeys("用户名输入框", "Admin").sendKeys("密码输入框", "1234567").click("登录按钮");

可以看到我定义了一个 page 的概念.一个页面所有的元素都在这个 page 里. 只要脚本中选定了某个 page,那么他就能随意控制页面操作. 那么 page object 在哪呢?脚本中我们看不到调用 page object 的操作,我们看不到你到底用 xpath 查找的元素还是用 id 还是用 name。请看下一段 xml 定义

<page name="登陆页面">
    <Element eleName="用户名输入框" xpath="//input[@type='text']" id="userNameInput"  ifBaseElement="true"></Element>
    <Element eleName="密码输入框" xpath="//input[@type='password']" ifBaseElement="true"></Element>
    <Element eleName="登录按钮" xpath="//button" ifBaseElement="true"></Element>
</page>

page object 在这里,这里面用中文定义了元素名称,以及控件元素到底是用什么方式去查找等信息。当脚本引用任何页面的时候,框架都会去缓存中读取此页面信息,并执行页面元素的控制操作。可以看到我们不仅把页面元素的定义分层出去,还把页面元素的查找过程也都分层出去了。 而且我们可以用自然语言定义控件的名字(英语还是汉语都可以),所以就像上面的代码一样,脚本在做什么一目了然。这就是可读性,我们做的事情跟之前没什么分别,但是我们把责任划分的更详细,脚本中只剩下业务逻辑。我们有一个原则就是脚本中只有业务逻辑。其他一切不相关的要不交给框架,要不交给其他层的模块。

使用类似 xml 这种可扩展性强的语义存储数据

我们看到上面的 xml 里还有一个 ifBaseElement 属性。 这个是什么呢? 它就是给这些页面元素打个标签,这些控件是属于页面基本元素,这样我们可以通过下面一段代码把所有带有这个标签的页面元素全找出来。

List<String> eleNames = driver.getBaseElementsNameOfPage("登陆页面");

for(String eleName:eleNames){
    WebElement element = driver.findElement("登陆页面."+eleName+"");
    Assert.assertNotNull(element);
}

看到效果了么? 这样我可以验证所有这些页面基本元素在页面中是存在的,这就是我们 UI 自动化策略中的静态元素验证。我们不用再一行一行去写代码验证了。而是通过 xml 这种方便扩展的定义遍历出所有的静态元素。这是一种方式,你也可以通过定义 xml 文件的属性扩展出很多功能。这是可扩展性。记得我的那篇数据驱动及其变种么?之后的关键字驱动框架就使用 xml 在数据驱动的基础上扩展而来的。同时 xml 是一种很清晰很结构化的定义方式。实际上 xml 本身的可读性就不低。可扩展性和可读性上去了,可维护性也就差不到哪去

代码复用:抽象一切可抽象的,减少一切可能的代码相似与重复

记住一点:代码越少越简单,维护起来就越方便。简单即是美
还是用 UI 自动化这个例子吧,我们看到上面讲 xml 可扩展性的时候。我们可以通过定义一个标签 ifBaseElement 来帮助做静态元素验证。但是 java 里普遍也就是用 dom4j 等工具遍历 xml 文件,你为 ifBaseElement 需要写一套遍历,你加另一个属性可能还要一套遍历。或者 xml 树结构改了,我们在已有的标签下又加了一套新的标签等情况。都需要重写遍历。而且一层又一层的 for 循环也挺让人崩溃的。外人不知道你这段代码在干吗。可维护性,可读性,可扩展性都差的要死。那我们一般怎么做呢。看下面一个例子。
:有个方案是写迭代器(请 Google 迭代器模式),for 循环过多,而且复杂的时候一般使用此模式增加可维护性和可读性。不过在 xml 遍历的场景中,应变能力不强。xml 变化,迭代器也必须变化。所以我一般使用解释器模式遍历 xml 和 json

XMLParser.parser(pageObject, "page/Element$(ifBaseElement=true).eleName");

OK,大家看到了吧,一个解释器接收一个 string 和 xml 对象为参数。String 就是我们自定义的语法,上面的意思就是取出 page 节点下的 Element 节点中所有 ifBaseElement 属性值为 true 的 eleName 属性的值。这样就满足上一个例子的遍历出所有的页面基本元素的需求了。通过定义一个简单易懂的语法(一开始我想做成根 sql 语句一样的语法的,后来觉得太麻烦了)满足了我们各方面的需求:使用者很容易使用,也很容易看懂这段代码再作什么。很容易接受变化,xml 改变了我们改变一下字符串就行了。扩展性也很好。语法很容易进化。基本上可读性,可扩展性,可维护性都做到了。
举些 json 的例子:

dataList[id=89898,54546,90723,1,90724,90725,54545]/*     取json中dataList数组中 id为这些的所有的值,*代表查询所有
dataList[id=89898,54546,90723,1,90724,90725,54545]/id   取json中dataList数组中 id为这些的所有的值,id代表只查询id
dataList[0~5]/*                                       取json中dataList数组中前6个元素
dataList[*]/*                                         取json中dataList数组中所有元素

想知道实现方式的自行 google 解释器模式吧,这个模式比较大,我说不清楚

再举个例子,我们写脚本的时候一定会验证返回值,有时候这个值可能是简单的数字或者字符串。有些时候可能就是复杂的对象了。这个时候对复杂对象作验证就比较痛苦,每个属性都写断言的方式简直要人命。为了解决这个问题,我们的方式是 java 反射机制加上责任链模式

VerifyHandler handler = VerifyHandlerFatory.createVerifyHandler();
String[] notverify = {"Task:id"};
handler.PassRequest(copyTask, sourceTask, notverify);

大家看上面的代码,就是比较两个 task 对象是否相等,第一行代码是创建一个责任链对象,第二行代码规定了什么东西不需要验证,因为 task 的 ID 是随机生成的不可能相等。最后我们把两个对象仍进去就行了。你不用管它怎么验证的,责任链在运行到 javaBean 类型的时候,就会用 java 反射解析两个对象的每一个属性并调用链表中其他的节点做相应的断言。是不是很好用?不仅仅是 javaBean 类型,JSON,数组,List,Map,File 你全都能不管三七二十一的仍进责任链里。这下子写脚本的人可爽了,以前我们最怕的就是一个 ORM 映射出来的字段百八十个的,光是写断言就写到手软。现在完全木有这个问题了。如果有新的验证类型出现,你只需要在责任链表里增加一个节点对应这个类型作验证就好了。不需要一大堆的 if else 递归调用的,可维护性,可扩展性很不错。现在可读性也好了,就一行验证代码,你肯定知道脚本在干吗。

说一下大概实现思路吧。责任链可以是单向链表,也可以是循环链表,甚至你可以发展成树形结构 (暂时我在测试中没碰见这种复杂结构,开发那常碰见),每个节点对应一种类型,如果判断当前类型是该节点应该处理得,就处理。如果不是就传递给下一个节点处理,依次类推,直到遇到跳出链表的点(例如验证结束)或者是到达链表的尾部。中间如果遇到容器类型例如一个 javaBean 或者一个 List 等等,就循环遍历每一个值依次传递下去。你可以理解为你为链表作线性遍历,但是链表给传递进来的对象做的事树的先序遍历 (深度优先)。

上图所有的类,所有的节点继承 VerifyHandler 抽象类,VerifyAlgoChain 是链表的容器,VerifyHandlerFatory 是组装链表的工厂类。下面贴一个 List 类型的代码

/**
 * 验证list类型
 * 
 * @author Gaofei Sun
 *
 */
public class ListType extends VerifyHandler {

    @Override
    public Boolean PassRequest(Object actualValue, Object expectedValue, String fieldName_no,Params info,String[] notVerifyFlag) {
        // 如果对象属于List类型就验证
        if (expectedValue.getClass().isAssignableFrom(List.class)
                || expectedValue.getClass().isAssignableFrom(ArrayList.class)
                || expectedValue.getClass().isAssignableFrom(LinkedList.class)) {

            List<?> expectedValueList = (List<?>) expectedValue;
            List<?> actualValueList = null;
            try {
                actualValueList = (List<?>) actualValue;
            } catch (ClassCastException e) {
                e.printStackTrace();
                Assert.assertTrue(fieldName_no + "返回值并不是List类型,而是:" + actualValue.getClass() + " 类型", false);
            }

            if (actualValue == null) {
                if (expectedValueList.size() == 0) {
                    return true;
                } else {
                    Assert.assertTrue(fieldName_no + " 返回值中的List为空,但是预期值不是", false);
                }

            }

            Assert.assertEquals("输入的List:" + fieldName_no + " 的大小与返回的不等", expectedValueList.size(),
                    actualValueList.size());

            VerifyHandler handler = VerifyHandlerFatory.createVerifyHandler();
            for (int i = 0; i < expectedValueList.size(); i++) {
                                // 取出所有对象继续在责任链表中传递。
                handler.PassRequest(actualValueList.get(i), expectedValueList.get(i), fieldName_no,info,notVerifyFlag);
            }
            return true;
        }
        // 不属于List类型,传递给下一个节点
        return nextHandler.PassRequest(actualValue, expectedValue, fieldName_no,info,notVerifyFlag);
    }
}

好了具体的实现原理请大家自行 Google 责任链模式
当然了大家也许会说我不用这个屁的责任链模式也可以阿,我写 N 个 if else 加递归调用加 java 反射也可以实现。那么我们通篇都在说什么呢?可读性,可扩展性,可维护性。如果你这么写你指望谁愿意接手你的代码。反正我写这种代码出来我老大肯定抽我。多少个公司的代码规范里都是严禁出现这种情况的

活用 java 注解和反射(python 中应该也有相关的机制)

这个例子是在 测试数据管理策略一章中讲到的注册式数据管理。看一下下面的例子

@DataBaseFile(filePath="defaultProject.xls",scope=Scope.CLASS)
public class UnitTestNew extends UnitCaseBase{

上面的代码在类的基础上加一个 DataBaseFile 的注解,然后再基类中我们有如下定义:

    /**
 * 根据数据文件内容解析出的数据库执行语句的集合,用来初始化和销毁数据库。 初始化方法读取数据文件执行数据库insert语句并给此变量赋值,销毁方法在测试结束后读取此变量执行销毁操作
 */
private List<DataEntity> dataEntityList; 
    /**
 * 表明子类的DataBaseFile注解
 */
private DataBaseFile data;
/**
 * 表明子类的DataBaseFile注解中数据文件的路径信息
 */
private String[] filesPath;
/**
 * 表明子类的DataBaseFile注解中执行初始化和销毁的策略信息
 */
private Scope scope;

    /**
 * 构造方法,获取子类的@DataBaseFile信息
 */
public UnitCaseBase(){
    register = false;
    data = this.getClass().getAnnotation(DataBaseFile.class);
    dataEntityList = new ArrayList<DataEntity>();
    if(data!=null){
        this.filesPath = data.filePath().split(",");
        this.scope = data.scope();
    }
}

我们可以看到,在子类中,我们使用注解的方式制定数据文件的路径和作用域,基类默认构造方法会使用反射的方式去读取注解的信息,然后再基类中定义好了方法去做测试测试数据的初始化和销毁。如下:

    // 供子类重写,用于setup测试用例
protected void methodSetUp(){}

// 供子类重写,用户销毁测试用例
protected void methodTearDown(ITestResult result){}

// 供子类重写,用于在测试类开始前执行初始化工作
protected void classSetUp(){}

// 供子类重写,用于在测试类结束后执行销毁工作
protected void classTearDown(){}

/**
 * 测试用例的初始化
 */
@BeforeMethod
protected void methodDataBaseSetUp(){
    this.setUpDataBase(Scope.METHOD);
    this.methodSetUp();
}
/**
 * 测试用例的销毁
 */
@AfterMethod
protected void methodDataBaseTearDown(ITestResult result){
    this.methodTearDown(result);
    // 判断子类是否注册了测试数据
    if(dataEntityList!=null&&register.equals(true)){
        this.destoryData();
    }
    this.tearDownDataBase(Scope.METHOD);
}
/**
 * 测试类的初始化
 */
@BeforeClass
protected void classDataBaseSetUp(){
    this.setUpDataBase(Scope.CLASS);
    this.classSetUp();
}
/**
 * 测试类的销毁
 */
@AfterClass
protected void classDataBaseTearDowm(){
    this.classTearDown();
    this.tearDownDataBase(Scope.CLASS);
}

我们可以看到基类定义的 before 系列的方法中有着针对数据作用域进行初始化和销毁的操作。并且留给子类接口扩展销毁和初始化操作。一般情况子类只需要使用注解规定数据文件的路径和作用域就可以了。这种基类定义行为,子类定义实现的方式是 模板模式 的变种. 这下我们可以看到我们的脚本类只需要继承这个基类,使用一个简单的注解就可以不用管数据的销毁与创建了. 我十分推荐这种方式制作测试框架.
活用注解和反射很重要, 很多工具和框架都离不开这两种机制

一个模块知道的越少越好

其实这个原则跟分层有点像,把责任划分出去了,知道的就少了. 现在让我们来看看下面一个例子,这个例子我之前关于数据驱动及其变种的帖子里的一部分. 用来解析作为数据驱动的 xml 文件中的数据类型,我们知道 java 是一门强类型语言,从 xml 读取出来的都是 String 类型,我们需要对其作类型转换. 如果我在脚本里做类型转换的话无疑太痛苦了.所以我们交给框架来做,我们希望脚本是这样的.

    @Test(dataProvider="unitDataProvider",dataProviderClass=UnitDataProvider.class)
@DataFile(filePath="test.xml")
public void test(List<String> in,File file, String mock,String out) throws SQLException{

如上面代码,测试脚本中用一个 DataFile 注解定义数据文件位置,框架用 java 反射读取内容,这一点和上面的例子很像. 数据类型的转换也交给框架来做.这样测试脚本就 知道的很少 然后我们再看框架. 框架专门有一个地方负责读取这些文件.然后解析并作类型转换操作. 可是我们发现这个类型转换操作其实是很复杂的. 你不确定你穿进来的对象里是不是还包含着另一个对象或者容器.java 里的对象就是个不确定深度的树结构. 我们一般的思路就是写 N 个 if else 判断到底是什么类型,然后做类型转换. 碰到容器类型就递归调用. 这样的话我们发现这个模块就非常复杂了.N 多的 if else 和递归调用无疑是个灾难. 之后不管谁来接手这段代码心里都会骂娘的.

所以我们把这个职责也分层出去成为一个模块. 我们不用那么多的 if else 了, 每个类型都单独一个类型转换算法. 提供一种机制让每一个算法之间都能互相调用 (模拟递归).所有的算法都实现一个接口.如下

import org.springframework.stereotype.Component;

import InterfaceTool.paramLoader.params.Param;

@Component
public interface TypeConvert {
    public Parameter convertType(Param value);
}

这个接口只有一个方法,就是类型转换算法. 所有的类型转换算法都要实现这个接口. 有一个工厂类专门负责创建算法对象.

public class TypeConvertFactroy {
    private static ConcurrentHashMap<String, TypeConvert> map = new ConcurrentHashMap<String, TypeConvert>();

    public static TypeConvert createTypeConvert(String type) {
        if (type == null || type.equals("")) {
            type = "String";
        }
        if (map.containsKey(type)) {
            return map.get(type);
        } else {
            TypeConvert obj = null;
            try {
                type = Tools.convertStringToUp(type);
                obj = (TypeConvert) Tools.reflectObject(
                        "InterfaceTool.paramLoader.typeConvert." + type + "Convert");
                map.putIfAbsent(type, obj);
            } catch (Exception e) {
                e.printStackTrace();
                Assert.assertTrue("没有找到 " + type + " 的参数类型,请核对是否输入错误的参数类型或请在系统中增加对应的参数类型", false);
            }
            return obj;
        }
    }
}

上面我们看到这个工厂类负责维护一个 map,map 里装的就是所有的算法 (缓存). 有一个细节就是

obj = (TypeConvert) Tools.reflectObject("InterfaceTool.paramLoader.typeConvert." + type + "Convert");

这是利用 java 反射去生成算法对象, 好处是以后加入新的算法类型的时候,只要在特定路径下定义一个特定名字的算法类就行了. 这样这个工厂类就能自动创建这个对象而不用任何的代码变动 (可扩展性)。

OK,那我们看看其他模块怎么调用算法的。

// 将Param转型成真正的参数
TypeConvert convert = TypeConvertFactroy.createTypeConvert(param.getType());
Parameter obj = convert.convertType(param);

上面我们看到调用方只要通过工厂类创建 TypeConvert 类型的对象就可以了。直接使用 convert 算法得到转型后的结果。调用方不需要知道他到底创建了哪个类型的算法 (因为工厂类返回的类型是所有算法的接口类型),不知道里面到底做了什么。只需要知道这么做是在做类型转换就可以了。这是最简单的 策略模式。对调用方来说,它知道的非常少,大家应该可以感觉出来这几个模块的可读性,可扩展性和可维护性了么?顺便说一下工厂模式的这种设计方法也是在模拟递归调用,也就是说算法内部调用其它算法的时候也是通过这个工厂类来调用的
上面的例子涉及到了表驱动,工厂模式,策略模式,注解,反射等知识,不清楚的请自行 Google

我发现篇幅已经好长了~,说最后一个例子吧。。还是用 UI 自动化那个例子。下面是代码

driver.page("登陆页面").sendKeys("用户名输入框", "Admin").sendKeys("密码输入框", "1234567").click("登录按钮");

大家可以猜到上面用的肯定不是 webDriver 原生的接口。我一定是封装过一层的,可我没有都重新定义 dirver 的接口(太多了,而且也没必要)。如果我们想调用原本的接口呢? 也简单,直接调用就行了(我没有重写 webDriver 的东西)。这时候大家可能猜到了,直接用继承来扩展 webDriver 不就得了。像下面这个样子:

public class MyChromeDriver extends ChromeDriver {

可是这样就出现问题了。我们知道 webDriver 里有好几种 driver。难道我们为了实现 page 这个功能要为每个 driver 都扩展一次么。肯定不行啊。多少的代码重复呢。 有的同学说你可以写个适配器阿,把 driver 传到适配器里不就行了。适配器里传得是所有 dirver 的基类类型就行了,在适配器里重新定义你需要的接口。像下面

public MyDriver(RemoteWebDriver driver, String pageName) {
        this.driver = driver;
    this.currectPage = pageName;
    pageObjectMap = PageObjectFactory.getPageObjects();
}

可是这样我们就无法通过这个适配器调用 driver 原本的接口了,而且我要让写脚本的人知道两套东西,一个 driver 和一个适配器,还需要用户去组装这个适配器。 也许有些同学说干嘛那么纠结,这样干活就足够了。但是我们是完美主义追求者,根据知道的最少原则我希望测试人员能通过简单易懂的方式完成工作。在这里我们让用户少知道点东西,在另一个地方我们让他们少知道点,聚沙成塔。最后我们的质量就上去了。OK,我们来看看到底怎么做。

首先我们知道,我们有了一个适配器,我们有了一个通过继承得来的 driver。下面我们要考虑怎么把他们两个合成一个东西。 我们知道 java 里是没有多重继承的。如果有的话我们就不用烦了。所以我们弄了个山寨版的多重继承 ---- 通过内部类。 看下面例子

public class MyChromeDriver extends ChromeDriver {
    private Map<String, Page> pageMap = new HashMap<String, Page>();

    @Override
    public void get(String url) {

        super.get(url);
    }

    public MyChromeDriver(ChromeOptions options){
        super(options);
    }

    public Page page(String pageName){
        // 如果缓存中已经有该page的对象就使用缓存中的,如果没有创建一个新的
        if(pageMap.containsKey(pageName)){
            return pageMap.get(pageName);
        }else{
            Page page = new Page(pageName);
            pageMap.put(pageName, page);
            return page;
        }
    }

    public Page page(){
        return new Page(null);
    }

    public class Page extends MyDriver{
        public Page(String pageName) {
            super(MyChromeDriver.this,pageName);
        }

    }

上面我们通过类内部再定义一个内部类,这个内部类继承了我们之前说的那个适配器类。这样我们就让一个类中拥有两个类的行为了。是不是很简单,我们把这个内部类命名为 page,适配器类中定义跟原生的 webdriver 一样的接口名称,例如,sendkey,click 等等。这样调用方很自然以为这其实就是一个机制。就像下面的代码

driver.page("登陆页面").sendKeys("用户名输入框", "Admin").sendKeys("密码输入框", "1234567").click("登录按钮");

符合人类思维的结构和接口命名,是不是看起来好多了。不想通过 page 的概念控制也可以 直接一个 driver.findElement 的方式去做。额,我嘴比较笨,大家自行体会吧。

case 的隔离

这回真是最后一个了。。。时间有限,我就简单点说了。 其实 case 的隔离主要分为两方面:第一,不依赖产品接口创建和销毁测试数据。第二:每个用例都有自己的测试数据,不跟其他 case 共享,最简单最有效的做法就是做 隔离数据,而且保险起见,case 运行结束后数据都删除掉。

具体实现还是看我那篇测试数据管理策略的帖子把。我说说为什么这么做,我发现其实大多数人都不是这么做的。就说第一点,很多人都是依赖产品接口做的测试数据准备。 测试这个接口前会调用很多别的接口先创建数据。其实这样你的测试粒度已经很粗了,你没有把被测功能隔离开,这是一个标准的粗粒度的集成测试。这么做不是说不行。而是一旦创建数据的接口 bug 了,你觉得你得有多少 case 会直接 fail。你得脚本里之前调用了那么多的接口,一旦 bug 了你能确定到底是哪个接口的 bug 么?举个例子,get 一个 user 的时候出错了,你能确定就是 get 接口出错了么? 很可能是 adduser 的接口就出错了。所以记得我们一开始就说的 测试脚本的目标 么,脚本能根据脚本准确定位 bug 的位置。

我们接下来再说第二点,很多人反对做隔离数据,觉得太麻烦没有必要。 可是我想说数据库是对所有 case 可见的。不做隔离数据的话,case 之前肯定互相影响。例如我这个脚本测试 listuser,把所有 user 都展示出来。 结果后来又测试一个 case 叫 adduser。adduser 就会导致 listuser 的失败。也许有些同学会说我们可以定义 case 执行顺序,我们可以定义不同类型的数据等等,但是这些都是不靠铺的,你的 case 可能依然互相影响。而且这些的前提是只有你一个人在做自动化,而且自动化的规模比较小。 如果是好几个人在做自动化,你能指望记住每个人的 case 执行顺序和数据类型么。 如果来个新人不知道这些规则呢? 如果 case 达到几千的量,你还记得住你的顺序和规则了么?

所以我一直在说数据的管理很重要,我在这一系列的帖子里第一篇就是测试数据管理是由原因的。

OK 就说到这把,篇幅好长了,其他的以后又机会说吧。分享的不好,大家见谅。最后做个总结,其实保证可读性可维护性可扩展性 的方法很多,我列出来的只是冰山一角。不仅仅是脚本的,还有框架的。都要保证这三个纬度。也许大家觉得这没什么卵用,不保证这些我照样干活,用不着学这些有的没的,什么设计模式数据结构的,都是用来装逼得。
我猜一定有很多人是这么想的。那么大家其实细想想,如果按我上面的方法做了,是不是长远上讲其实是增加了效率的。 一旦产品改动,架构改动或者产品出 bug 的时候。会不会很爽? 如果大家仔细想想得话,答案是肯定的。 退一万步讲,如果你的代码里真的是各种 if else for 循环的嵌套好几层,还外带递归调用的,没什么分层没什么模块的。这样的代码你指望谁来接手? 时间长了你不忘么?就算你不忘你能保证你呆在公司一辈子么?你走了留个烂摊子给后辈么? 我们不要嘴里骂着开发的代码质量垃圾的同时,自己还写着垃圾代码吧

So,大家不要排斥学习开发的知识,一旦经历过数千级别的 case,长达数年的自动化项目,遇到过这样那样的坑的时候。你就会知道我说的这些有多重要。

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 37 条回复 时间 点赞

额。是我写的字太多了大家不爱看么

来了来了,刚下班 来捧场了

学习了。

这里就能体现设计模式的重要性了
小项目不在意也不会有什么问题,大项目比较注重这些。。每次看完前辈你的帖子,都会有种不滚回去学习不行的心理 →_→

涨姿势了~

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

太棒了, 真正的干货,慢慢看

小白学习之路。感谢。

能不能拜师学艺~

#8 楼 @cat_1218 额,感谢你觉得我很厉害...

@ycwdaaaa 正在努力学习中,谢谢你分享的每篇帖子

不错,对于想玩测试框架的人来说是一个好的思路

学习了,滚回去看你说的那几种模式了!!!

真的是干货文章,涨姿势

请教下楼主 关于 dubbo,rmi 协议的接口自动化测试如何开展,针对 java 嵌套复杂的类,测试数据如何构造?返回结果如何验证?

#1 楼 @ycwdaaaa 都这么多赞了 还不满意啊. :)

请教楼主大神有 Python 方面的干货没,就像你上面介绍的 java 的内容一样

#16 楼 @darkcao 语言是相通的

#15 楼 @seveniruby 啊。满意。。很满意

#16 楼 @darkcao 都差不多吧应该。python 应该也是相似的

通读了一遍,觉得有点难消化,看来我得好好学习啊~

希望楼主持续分享宝贵经验~(__^)

@ycwdaaaa , 很好的文章,有 1 个问题请教下
XMLParser.parser(pageObject, "page/Element$(ifBaseElement=true).eleName"); 这个为啥不直接用 xpath 的类库来解析,要自已做一套解析?selenium 自身有类似的从 xpath 拿出元素的功能吧?

#21 楼 @liujiong 这个是为了解析 XML 文件,之所以自己做而不用 xpath 是为了定制一些功能。例如直接把 xml 标签中的属性取出来后封装到一个对象里。例如下面这个例子:

List<ServiceEntity> serviceEntityList = (List<ServiceEntity>) XMLParser.parser(monitorFile,
                "monitor/environment(name=test)/service.*", ServiceEntity.class);

这段代码就是取 xml 文件中符合条件的所有标签的属性,并把这些属性封装到 ServiceEntity 这个对象中,返回一个 ServiceEntity 的 List。 这样我可以一次性取多个标签的多个属性值

写的不错,不过确实长了一点。如果对整个框架或背景不了解的话,不太好理解吧。
对于复杂 Java 对象的比较我觉得直接重写 hashcode() 方法,用 equals() 比较就好了。责任链没太理解,感觉还是要写很多 if else 来判断具体类型,只是封装性更好。

楼主 java 知识好牛逼,请问怎么才能这么牛?

太长。。。。用一句话概括,MVC 模式!!!比较适合做框架原型!

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

#24 楼 @erickyang 额。别神话我。我远没到大神程度

#26 楼 @ycwdaaaa 已经很厉害了,默默向你学习中。。。

为什么大家都写 “登陆” 呢。我看多了都成强迫症了

30楼 已删除
孙高飞 [该话题已被删除] 中提及了此贴 06月28日 18:51
孙高飞 [该话题已被删除] 中提及了此贴 06月28日 18:51
孙高飞 [该话题已被删除] 中提及了此贴 06月28日 18:51
孙高飞 测试开发之路 ---- 框架中数据的管理策略 中提及了此贴 12月02日 10:47
孙高飞 测试开发之路 ---- 数据驱动及其变种 中提及了此贴 12月10日 11:45
徐旻 突发奇想之 judge_object 中提及了此贴 02月23日 10:29
<page name="登陆页面">
    <Element eleName="用户名输入框" xpath="//input[@type='text']" id="userNameInput"  ifBaseElement="true"></Element>
    <Element eleName="密码输入框" xpath="//input[@type='password']" ifBaseElement="true"></Element>
    <Element eleName="登录按钮" xpath="//button" ifBaseElement="true"></Element>
</page>
  • 如果是登录需要多个用例,那么就要重新复制 - 粘贴上面的大部分代码,只是检查点和输入的用户名或者密码不一样?
  • 如果是一个用例有多个检查点,要如何配置?

额,真没想到都这么久了还有人回复。 其实这个东西我现在已经不用了,也是有一些缺点的。 但我还是跟你说说吧。 这个东西其实就是个 page object 的加强版。只对页面元素做定义的。case 里不用它的

我也在学习中,感觉用这个进行元素管理还是比较干净的。但是要写一个解析 xml 的 class。 尤其是那个 ifBaseElement 这个主意绝对是可以的。在运行 case 前 进行一次元素排查,如果元素没有找到,说明页面被动过了,case 肯定需要修改了。免得跑完了发现报错都是找不到元素,还要跟着 log 慢慢回去找。

孙高飞 回复

说明一下缺点吧。超级感兴趣。

孙高飞 回复

同问,有啥缺点呢,哪些方面可以优化

有没有人用 python 实现解析楼主 xml 结构的方法,卡在这里一脸懵

徐旻 回复

主要是重构方面的。当初我的同事用惯了 eclipse 了。 eclipse 的重构功能不能覆盖到字符串搜索。所以元素变量名改变的时候比较麻烦。不像 idea 中可以全局搜索字符串来帮你进行重构。我也不好强迫他们改用 idea。

古丶月 回复

看我上面的回答哈

孙高飞 测试开发之路----概要 中提及了此贴 03月13日 13:13
在路上 AvatarWebUI 自动化测试工具 中提及了此贴 06月01日 18:35
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册