背景
项目是基于Ruby on Rails开发的web程序,应该说项目中的测试实践是很好的,具有高覆盖率的单元测试以及比较合理的集成测试。存在的问题是,所有的单元测试和集成测试都是针对后端代码的,前端的JavaSript代码没有单元测试(这个是有历史原因的,暂时没法改变)。这也就意味着针对前端UI的修改是没有底层的单元测试来保障的,只能依靠高层级的UI自动化测试和手工测试来保障。
我们最近刚刚完成了一个story,是纯前端的开发工作,结果在上线后发现我们在修改页面模板文件时,忘记了其他地方也在使用同样的模板文件,导致了部分页面样式错乱,甚至无法访问。这个bug在现有的UI自动化测试和手工测试中都被遗漏了,直接发布到了产品环境上,后来通过线上日志才被发现。不得已我们只好把版本先回退,修复bug后再重新上线,release的时间推迟了一周,可以说整个团队为此付出了严重的代价。
痛点
作为团队的QA,出现这样的问题自然让我很不好受,颜面无光。。。抛开没有做story业务分析,手工测试场景遗漏等问题,聚焦在现有的UI自动化测试上,以下是我感知到的一些痛点:
UI自动化测试的稳定性不高 - UI自动化测试是用ruby版本的selenium实现的,存在着众所周知的问题,例如不够稳定,经常由于页面加载超时,UI交互复杂等问题导致测试失败,使得自动化测试结果的有效性不高,失去了它本来应该具有的价值。
测试场景覆盖不够全面 - 当然,根据测试金字塔的理念,上层的UI自动化测试应该是小规模的,只覆盖基本功能:
但是具体到这个项目中,由于前端代码的底层测试缺失,所以不得不依靠高层级的UI自动化测试来覆盖更多的场景, 最起码要能完成基本的回归测试,否则手工测试的压力太大。这就回到了第一个问题,正是由于UI自动化测试的稳定性不高,所以我只实现了很小一部分功能,以免后续维护的成本太高,而我们遗漏的bug恰恰没有包含在自动化测试的场景中。
问题分析
所以,我们的聚焦点集中在了如何提高UI自动化测试的质量上,经过分析,主要的问题有:
页面加载时间超时
在用webdriver的方法访问页面时,经常出现加载超时的问题。页面的DOM结构已经渲染完成了,但是在访问某些外部网站时长时间没有响应,导致测试脚本一直卡着无法继续进行,最后报超时错误。
页面交互action太多
UI自动化测试的一个缺点就是所有的执行动作都要通过页面的action来完成,例如文本框输入,点击按钮,而且这些动作的执行结果存在太多的变数,导致最后写出来的测试脚本太过脆弱,很容易失败。
解决问题
使用异常捕获机制来处理超时等异常情况
针对页面超时的问题,由于页面的DOM结构已经渲染完成了,所以其实可以继续执行后续的用例。但是如果与外部网站的请求没有全部完成,selenium会认为页面加载没有完成,就一直卡在访问页面的get方法上。我的做法是设定一个页面加载的超时时间:
dr.manage.timeouts.page_load = 30
超过30秒就认为页面加载超时(经过测试,30秒的时间已经足够把页面的DOM结构渲染完成了),然后访问页面时捕获超时的异常:
begin
page.open_page
rescue Selenium::WebDriver::Error::TimeOutError
puts "ERROR:page load timeout!!"
end
这样如果加载超过30秒,会抛出异常后继续向下执行。不致于因为访问某些外部网站请求过慢,而卡在页面加载的步骤,导致整个测试用例失败。同样的策略也可以应用在某些复杂或者不确定的执行结果上,捕获可能出现的异常,提高测试的健壮性,避免非产品原因导致的测试用例失败。
减少UI测试,增加API级别的测试
这个观点听上去多少有点荒唐,提高UI测试质量的方法是减少UI测试。。。。但事实就是这样,UI测试是自动化成本最高,最不稳定的测试,能够在低层级完成的测试,例如API层的测试,就不要放到UI层去完成,这样会使测试更加稳定和健壮。很多页面action,例如提交表单,点击按钮等,实际都是向后台发送了一条请求,如果测试的是功能,那么完全可以用API来完成这些测试。重点还是在于我们究竟想测什么,是否必须要从UI层去完成这些测试?这就是测试结构的设计问题了,这里不展开讨论。
在这个项目中,我把一些可以在API层完成的测试从UI测试用例中分离出来,用ruby的Faraday库(当然也可以用其他的,例如JS的SuperTest)调用相应的API并对返回数据做校验,这些测试相比于UI测试更加稳定,运行速度更快,整个测试的稳定性便得到了提升。
将页面交互的action与静态页面内容分开测试
这个方法的思路和上一个类似,尽量减少通过页面加载和交互来完成的测试。如果测试内容中不包含动态的页面交互步骤,例如只是想测试页面能否正常打开,某一部分的内容能否正常显示等,可以从页面的DOM结构中通过校验某些元素来完成测试。
举个例子,如果想测试百度首页,可以不用从selenium webdriver中去加载这个页面,直接用ruby的Faraday或者JS的SuperTest去访问"http://www.baidu.com"这个URL。这样拿到的将是一段html的文本,然后再解析这段html文本(例如用ruby的Nokogirl库),获取对应的内容来做校验。例如返回码是200,<title>的内容是“百度一下,你就知道”,那么可以认为首页能够正常打开。<img>中的src属性是一个正确的图片文件,可以认为百度的logo能够正常显示。
这里会产生疑问,如果我就是想测试界面怎么办?这就回到了刚才那个问题,我们究竟想测什么?如果只是想测功能,那么我们就尽量减少对界面的依赖。如果只是想测界面,那么也有其他的办法来完成,例如WebdriverCSS或者PhantomCSS等界面对比的测试工具。当然,有些测试步骤是必须要依赖页面交互的,例如点击某个按钮打开一个新的对话框或者跳转到其他页面,这些测试就只能通过webdriver来完成了。
总结
通过实际的尝试,我明显感觉到优化后的自动化测试相比于原来有了更稳定的表现,运行速度变快,非产品原因导致的用例失败次数变少。而且自动化测试更稳定后,我有信心去实现更多的自动化测试,扩大覆盖的场景。我的一些感受和收获是:
1.UI自动化测试的编码实现不难,难的是如何做整体的测试结构设计。在UI测试中覆盖的场景太少达不到测试的目的,场景太多又会成为团队的负担,带来高昂的维护成本,需要和UT/API等测试综合考虑,互相弥补。
2.UI自动化测试是把双刃剑,它应该是QA最后考虑的自动化测试方法。只有当其他层级的测试无法达到目的,或者希望测试的内容必须要通过UI完成时,再去考虑用它。低层级能完成的测试不要放在高层级去完成,在静态DOM结构中能完成的测试不要通过webdriver加载页面去完成。