大师兄

04|脚本复用:什么样的代码才值得写?

你好,我是柳胜。

开发和测试团队我都带过,现在测试人员的代码能力越来越强了,已经接近开发人员,我看过一些测试牛人设计的测试模块和代码:MVC实现测试控制台、分布式多测试节点管理、Proxy对测试Interface默认实现……这些用到的技术栈和开发不相上下。

聊下来,很多的反馈是“能做起来,很庆幸有一个对测试技术很支持的领导”。这就是一个让人困惑的问题,为了开发自动化测试案例,你写了那么多代码,那每一行代码就应该存在它的价值,这个价值是客观的,而不是依赖主观的某个人的认知。不是么?

所以这一讲要关注的问题是,你写的每一行代码都有自动化测试的价值么?你能把它说出来,说清楚么?想清楚这些,你自然也会明白给定一个自动化测试的项目,哪些工作是overwork(过度工作),哪些是underwork(工作得还不够)。

哪些代码值得写?

在开始之前,我们再回顾一下自动化测试ROI模型。

图片

一个案例转化成自动化测试后,我们的目标是它的投资回报率越高越好,在ROI公式里,回报也就是分子越高越好,成本也就是分母越低越好。

在第一讲我讲到过,n是自动化测试案例运行的次数,在回归测试里,n是回归迭代的次数,回归次数越高,n也就越大,这是从时间的角度上来看n。在这一讲,我们换个角度,从空间来看,也就是代码的复用率,我们有没有办法让代码的复用率升高?

初始版本

为了让你更容易理解,我们结合一段登录的脚本,一起探索一下怎样提高一个自动化测试案例的复用率。

脚本代码如下:

@Test
public void login() {
WebDriver driver=new ChromeDriver();
driver.manage().window().maximize();
//打开页面
driver.get("https://www.example.com/users/sign_in");
WebElement username=driver.findElement(By.id("user_name"));
WebElement password=driver.findElement(By.id("user_password"));
WebElement login=driver.findElement(By.text("登录"));
//输入用户名
username.sendKeys("liusheng@example.com");
//输入密码
password.sendKeys("123456");
//点击登录按钮
login.click();
}

这段脚本实现的功能很简单,启动chrome浏览器,打开一个登录链接,在页面上输入用户名liushing@example.com,密码123456,点击“登录”按钮,完成登录。此处我省去了assert检查点。

现在,这段脚本每运行一次,就测试一次登录。它的n现在就等于1。我们想一下,这个脚本还能怎么提高它的复用率呢?

提高复用率:一份代码,多浏览器运行

可以看到,脚本运行的测试案例只在chrome上,但作为一个web应用,一般是要支持市面上主流的浏览器,看一下阿里云网站支持12种浏览器,列表如下:

图片

那么,有没有办法让我们的脚本能够一下子测试12种浏览器呢?此时我们需要修改脚本,支持调用多个浏览器driver:

@Test
@Iteration(Driver=ChromeDriver,FireFoxDriver.....)
public void login() {
//此处是伪代码,代表从Iteration数组里拿到的driver元素
WebDriver driver=new Iteration("driver");
driver.manage().window().maximize();
//打开页面
driver.get("https://www.example.com/users/sign_in");
WebElement username=driver.findElement(By.id("user_name"));
WebElement password=driver.findElement(By.id("user_password"));
WebElement login=driver.findElement(By.text("登录"));
//输入用户名
username.sendKeys("liusheng@example.com");
//输入密码
password.sendKeys("123456");
//点击登录按钮
login.click();
}

上面是伪代码,代表Test会重复运行Iteration数组,driver的数目有多少个,就运行多少次,每次运行从iteration数组里取得driver的名字,交给脚本去启动相应的浏览器。

现在dirver有12个,我们就可以让一份脚本测试12个浏览器,获得了n1=12。

总结一下,最佳实践:一份代码,兼容多个浏览器。

提高复用率:一份代码,多数据运行

刚才已经迈出了第一步,不错,我们再继续看脚本,还有没有可以改进的地方。现在,我们的脚本只能测试一组用户数据,用户名liusheng,密码是123456。在测试方法论中,一个测试案例应该有多组测试数据,那合法的用户名的数据格式不止这么多,按照字符类型划分等价类,至少有5组:

1.ASCII字符
2.数字
3.特殊字符
4.拉丁文字符
5.中文字符
你如果有兴趣想知道为什么这5种字符是等价类,可以去研究一下字符集原理。

密码一般是数字,ASCII字符加特殊字符3种。我们至少可以开发出5*3=15种合法的用户名密码组合,作为测试用例。

@Test
@Iteration(UserPassword={xxxx,123456},{测试用户,Welcome1}....)
@Iteration(Driver=ChromeDriver,FireFoxDriver.....)
public void login() {
//此处是伪代码,代表从Iteration数组里拿到的driver元素
WebDriver driver=new Iteration("driver");
driver.manage().window().maximize();
//打开页面
driver.get("https://www.example.com/users/sign_in");
WebElement username=driver.findElement(By.id("user_name"));
WebElement password=driver.findElement(By.id("user_password"));
WebElement login=driver.findElement(By.text("登录"));
//输入用户名
username.sendKeys(UserPassword[0]);
//输入密码
password.sendKeys(UserPassword[1]);
//点击登录按钮
login.click();
}

上面的代码是伪代码,UserPassword数组有多少个元素,测试案例就会运行多少次。每次运行会从interation里取得数组的一个元素,一个元素就是一种用户名密码组合。

上面的UserPassword数组有15个元素,我们的测试案例就运行15次,现在n2=15,加上浏览器的12次,n=n1+n2=15+12=27次。

最佳实践:一份代码,多组测试数据。

提高复用率:一份代码,多环境运行

现在我们已经摸着道了,提高ROI,那就是让一份自动化测试程序,尽可能多复用在不同的测试场景中。这些测试场景本来就是有效的测试需求,转换成自动化也是一劳多得。

还有没有其他场景呢?当然有,举个例子,在我们的产品发布pipeline里,贯穿了从开发环境、测试环境、准生产环境到生产环境,由低向高的交付过程。

那你的测试脚本需要兼容每一个环境,在所有需要运行它的环境里都可以直接跑,不需要做任何修改。一份脚本,运行在dev,test,stage,productin 4种环境下,我们的n3=4, n=n1+n2+n3=15+12+4=31次

你可能会说,这个有难度呀,环境不同,不光是脚本url不同,里面的测试数据也不一样,测试配置也不一样,甚至timeout要求也不一样等等,不是那么容易实现的。那你可以想想,这种问题,是不是开发也会遇到,他们需要把服务部署运行在不同的环境下。开发是怎么做到的?

以SpringBoot为例,它提供了多profiles配置的功能,application.yml文件配置默认参数,application-dev.yml里放dev环境的配置参数,application-test.yml放test环境的配置参数,application-prod.yml里放production环境的配置参数。

当spring boot application启动时,会自动根据输入环境参数,加载相应的环境yml配置文件。这就实现了一份代码,多环境部署的场景。

我们的自动化测试也可以实现类似的机制。聪明的你,可以考虑自开发一个测试配置文件加载模块,代码不需要多,但会直接增加ROI。

最佳实践:一份代码,兼容多环境运行

提高复用率:一份代码,多语言运行

另外,如果你的产品支持多国语言,那么一份代码跑多国语言版本,也是一个会显著增加自动化测试ROI的好主意。

像上面的代码,当前只支持中文页面。假设我们的产品要求支持9种语言,那可以让页面控件加载不同语言的label text。

伪代码如下:

@Test
@Iteration(UserPassword={xxxx,123456},{测试用户,Welcome1}....)
@Iteration(Driver=ChromeDriver,FireFoxDriver.....)
@Iteration(Profiles=auto-dev.yml,auto-test.yml,auto-prod.yml....)
@Iteration(Language=en,zh_CN,zh_TW, FR....)
public void login() {
//此处是伪代码,代表从Iteration数组里拿到的driver元素
WebDriver driver=new Iteration("driver");
driver.manage().window().maximize();
//打开页面
driver.get(profile.getUrl());
WebElement username=driver.findElement(By.id("user_name"));
WebElement password=driver.findElement(By.id("user_password"));
WebElement login=driver.findElement(By.text(Label.getLoginText(language)));
//输入用户名
username.sendKeys(UserPassword[0]);
//输入密码
password.sendKeys(UserPassword[1]);
//点击登录按钮
login.click();
}

现在一份脚本经过了多浏览器、多数据、多环境和多语言4轮打磨,运行的次数n=n1+n2+n3+n4=12+15+4+9=40次。如果各个场景有关联关系,比如页面的语言和测试数据有耦合,英文页面的encoding和数据的charset有关联,那么两个场景的次数就是完全组合,采用乘法,15*9=135次。

而且,从脚本的变化可以看到,脚本第一版本里的hard code也一个个被消除了,取而代之的是数据驱动。消除hard code是提升ROI的结果

当然,上面的代码都是伪代码,实际上为了支持多场景运行,付出的努力不止是增加循环那么简单。比如,支持多少种浏览器取决于框架,而不是脚本。像现在有一些新的基于JavaScript的测试框架,只支持chrome浏览器,开发人员很喜欢用它来验证功能,但从自动化测试角度来看,它的ROI就受限了,这些局限都应该框架选型时考虑进去。

还有哪些工作值得做?

第一讲提出ROI模型的时候,我就提过,成本里的维护工作量是一个不确定的风险。根据ROI金字塔模型,维护的工作量也是自底向上增加,不确定性增加。

图片

维护工作量的不确定性是自动化测试的一个重要风险,所以我们有必要看一下维护的工作量都花在哪里了。

1.被测截面发生变化带来的维护工作量。比如UI自动化测试的产品页面发生了变化,API自动化测试的接口做了重构。

2.诊断自动化测试的工作量,如果把自动化测试结果分为真阳,假阳,真阴,假阴。那假阳和假阴都是需要诊断的。

诊断是要花费时间的,有这么几块:

1.从错误实际发生,到我们知道错误发生,这有一个通知的时间;
2.从开始诊断错误,到定位出错误,这要花费一个诊断的时间;
3.修复错误和验证修复方案,这也要时间,即修复时间;
4.修复上线后,跑出第一轮测试结果,证明完全恢复,这叫确认时间。

图片

怎样能提高诊断的速度呢?从上面的分析,可以看到:

1.缩小通知时间,目标做到实时通知。一旦有错误出现,应该立刻有责任人被通知到,进入到诊断环节。

2.诊断越快越好,一旦确定为自动化测试脚本的问题,应该立即对团队做出说明,并将错误案例下线,避免持续出现相同错误,引起误解。

3.修复要彻底,一个案例持续不能给人信任的结果,将会打击团队的信心。我见过有的公司,自动化测试跑完一遍后,还要手工再去验证一遍,这样的自动化测试就失去了价值。

4.确认后,自动化测试快速恢复上线,开始使用。

你在这里,可以发挥技术能力,按照上面的方向努力,去降低自动化测试的维护成本,比如:

1.Log规范+ELK+Grafana实现告警实时传达。
2.检查点+日志+屏幕截图甚至视频,提高诊断效率。
3.高内聚低耦合的模块化设计,能够实现隔离错误,缩小影响范围,快速修复的效果。
4.自动化测试上下线的标准和流程的建立。

图片

小结

今天我最想和你传达的观点是:在提高收益的方向上,我们付出的每一份努力和尝试,都是值得的。

其实自动化测试有个特点,它并没有一个显式的可验收交付目标。我们既可以写几行脚本就自动化一个案例,也可以实现一个支持控制台、多代理、持久化等功能的复杂系统。

但是别忘了自动化测试的本质,它是一个有业务需求的软件实现。这个业务需求以前没有人去讲清楚,学过这一讲,你就应该明白了,业务需求就是自动化测试ROI,想办法提高它是这个软件的唯一目的。

我讲到了提高代码ROI的通常的四种思路,一份代码,多浏览器运行,多数据运行,多环境运行,多语言运行,并用例子向你展示了,这个提供ROI的过程,也是消除代码Hard code,优化代码结构的过程。

另外,降低维护成本,也是提高ROI的一个非常有效的办法。我们分解了自动化测试的诊断时间,分为通知、定位、修复和确认四块时间,针对每一块时间,都有相应的办法去提高效率,这里我列出了一些常见办法,帮你在工作中找到更精准的目标,你可以保存下来做个参考。

总之,任何能够提高ROI的代码都是有价值的,反之,就是overwork。作为自动化测试架构师的你,在头脑里应该存在一个优先级从高到低的工作列表,先做哪块,再做哪块,按照ROI从高到低的顺序来安排。

你也应该能明白,在一个十几人的团队里,去做一个大规模的自动化测试项目,回报是追不回投入的,这样的项目即使能立项,最后也是死掉。反过来,如果你的领导愿意支持你去做一个复杂的系统,那他要么有一个推广提升的配套计划,要么是一个技术fans,像曾经的我一样,不尊重自动化测试的本质,也会败在ROI的规律手下。

思考题

回到这讲开篇的问题,思考一下你目前负责的自动化测试项目,是overwork还是underwork?

欢迎你在留言区和我交流互动,也推荐你把这讲内容推荐给身边的测试同学,一起精进技术。