行为驱动开发之四,为自动化测试(运行Cucumber)提速
六个月前,开始推广BDD。时至今日,已经有了1200个情景(Scenario)。如果把每个情景,当做一个自动化测试用例,那么短短半年,我们已经从无到有,开发了1200个测试用例。在没有BDD的过去五年中,我们一共才开发了2000个自动化测试用例。粗略地计算下:
- 过去2000/5=400个/年
- 现在1200/0.5=2400个/年
即BDD使我们的自动化开发效率,提高了五倍,是原来的六倍。(等我先我自豪一下,hiahia。)
越多越慢
慢着!还不能高兴地太早。高效的自动化开发,带来了新的问题。突然一下子多起来的测试用例,让自动化执行时间变长了。每次的nightly build/test的时间,竟然已经接近12小时。如果加上双平台,那么跑上一轮自动化测试,要整整等上一天才能知道结果。在推广持续集成的现在(推广CI的博客,我会陆续登出),只要有代码提交,便运行的自动化测试,竟要耗上一整天,才能对提交的代码有一个反馈?!这不管对测试人员,还是研发人员,都是不可以接受的。
自然,最简单的解决办法,少跑。从每个Feature(功能)中,精心挑选出最基础的Scenario(场景),每次只运行这些基础的场景。这样子,便无法叫做回归测试,顶多算是验收测试,或者冒烟测试。这个做法,大家说的烂大街了,挑呗,没啥难的。
可我不偏不要烂大街!自动化测试开发的意义,就是要多跑!多测!不间断地验证提交代码的正确!回归正确!要真是写了个脚本,一个礼拜跑一次,那也太金贵了!因此,一场轰轰烈烈的为自动化测试提速的运动展开鸟。
以下按照提速有效性,降序排列,越往后,提速越小。
提速法1:降低Setup/Cleanup的层次。
举一个在Eclipse的workspace中创建Java文件的例子。
Scenario: create java file in ws Given I have an Eclipse workspace When I create a new java file with name "Student" Then eclipse should create a file with name "Student.java" as: """ public class Student { public Student() { } } """
修改前,此处的step:
Given /^I have an Eclipse workspace$/ do eclipse.new_workspace() end
但是,当测试用例变得越来越多时,每个用例都需要"Given an Eclipse workspace",这时候,就要思考一下,有没有什么底层的,快速的办法,生成一个空的"workspace"呢?答案是,直接写文件。因此,我们大可以将上述step,改为:
Given /^I have an Eclipse workspace$/ do system("cp -r #{$empty_ws} .") end
以此类推,需要“delete_user"的地方,大可以直接delete database,甚至可以使用数据库镜像与回滚(db snapshot/rotate)的工具。理由:我要测试的步骤是"create a new java file",而前后的步骤,只是我所需要的环境。搭箭环境的步骤,以快,准,稳为原则,尽量避免免调用系统接口浪费资源与时间。
提速法2:提高Setup/Cleanup的层次。
在降低了部分Setup/Cleanup的层次后,我们会发现,有些Setup/Cleanup,是不需要每一个Scenario都要运行的。如远程Login的链接,如登录Web页面的链接等等。hook.rb中,可能表现为:
Before do @cli = Cli.login(host, user, password) @web = Web.login(url, user, password) end After do @cli.logout @web.logout end
这些链接是属于无状态的,并不会因为这次提交了某个cmd,或者http request而影响链接本身。为了避免重复登录、退出,可以在一开始(所有Scenario开始前)就登录,而在结束后(所有Scenario结束后)退出。改写后的hook.rb为:
@cli = Cli.login @web = Web.login at_exit do @cli.logout @web.logout end
这样,就避免了每次登录、退出所需要的时间。
提速法3:干掉sleep。
这个本来不想说,不过我搜了一把,sleep还真是多啊。去掉每个sleep的方法都有不同,这里要根据具体情况分析,就不赘述了。实在不行,sleep这一类的Scenario,可以打一个标签,叫@long,或者@sleep,在运行时,去掉这类标签。将其由每小时运行一次,改为深夜运行,每天一次吧。
提速法4:减少模块依赖。
控制集成测试与系统测试的用例数,对于提高自动化运行速度,很有帮助。如下面的例子:
Scenario: user action should be logged Given user login bank website When user transfer "20" bucx to stock Then bank should log as: | userid | mount | date | | $user | 20 | today|
开始时,我们的step为:
Then /^bank should log as:$/ do |ast| ast.hashes.each do |hash| sleep 5 logdb.search(hash).should == true end end
这里的问题是:引入log系统的数据库。数据从web系统,到log系统,最后被存入数据库,耗时较长,且不定时常。只能靠长时间的sleep来解决。如果将所产生的日志写在文件里,只检测文件,那么速度就快多了。修改后,step为:
Then /^bank should log as:$/ do |ast| ast.hashes.each do open("log", r) do |f| f.read.should.include ("userid"+hash["user"]+"mount"+hash["mount"]+"date"+hash["date"]) end end end
当将log系统剥离之后,这个Scenario的执行时间变短了,更提高了模块化的程度。即,如果log系统当前版本不稳定时,依然不会影响web系统的测试。
提速法5:精简数据
这可能是大部分测试人员会发生的问题,用例的过分复杂,导致step(步骤)太多,或是data(数据)太复杂。如:
Scenario: regiestered user login Given registered users as: |user | email | password | | a |a@cnblogs.com | 123 | | b-B_|b-B_@cn.com |3445 | | c |c@haha.com |1 | When users login with password: |user | password| | a | 123 | | b-B_| 3445 | |c |2 | |d | 1233 | Then users should see: |user | msg | |a | welcome | |b-B_ | welcome | | c | incorrect password| |d | invalid user |
这个用例的问题是,测试重点不突出,导致了冗余数据。从Scenario书写的意图看,即想测试各种用户名,又想测试合法与非法用户登录。为了突出测试意图,我们可以将它分解:
Scenario Outline: different user name login Given regiestered users as <user> and <email> and "123" When user login with password "123" Then user should see "welcome" Scenarios: |user | email | |a |a@cnblogs.com| |b- |b-@cnblogs.com| |c_ |c_@cnblogs.com| |B |B@cnblogs.com | Scenario: unregiestered user login fail Given unregiestered user "a" When user "a" login with password "123" Then user should see "invalid user"
之所以把它排在最后,是因为精简了数据,很可能导致一个超级复杂的Scenario,变成若干个小的简洁的,测试意图明确的小的Scenario,却不保证,运行时间一定会缩短。看着运行Cucumber的时间一天比一天少,心情也越来越好。傍晚路过咖啡店时,买了个很甜的东西奖励自己。可直到吃完,都不知道那东西叫什么名字。