谈谈selenium的脚本组织形式。selenium的脚本写法只有三个步骤:定位、等待和操作。当我们用它来做测试脚本时,还需要加入第四步骤:断言。selenium的脚本组织常用页面对象模式page object,或关键字模式keyword driven,但今天还要讲第三种,即返璞归真写脚本。
selenium脚本写法
selenium的脚本写法只有三个步骤:定位、等待和操作。
一,首先是“定位”,先把web页面上的元素定位到,不管用什么语言写selenium脚本,都是差不多的,方法名为 find element by xxx,也就是通过xxx来定位元素。可以是id,也可以是name,还可以用xpath和css来定位。
例1.文本框的html
<input type="text" class="s_ipt" name="wd" id="kw" maxlength="100" autocomplete="off">
比如这是百度首页一个文本框的html语句,要定位它,
- 可以用id:find_element_by_id("kw")
- 也可以用name:find_element_by_name("wd")
- 还可以用xpath;find_element_by_xpath(某个xpath表达式)
具体的xpath表达式语法请读者自行搜索,这里就省略了。xpath可以用来在html等标记语言中定位节点。换句话说,用id和name定位不了的,就全用xpath就行了。
4. 当然其他还有很多定位语句和定位方法,但是都是可用可不用的。
元素怎么也定位不到?
1.看有没有frame,2.尝试actions操作,3.让selenium调用原生js搞定。
二,然后是“等待”。
selenium提供的隐式等待和显式我们都不需要,因为实用效果不好。我们自己实现一个等待就好了。思路就是,先定位,如果失败就sleep 若干秒然后重试,直到最后定位成功或超时。
比如我们按照robotframework里的等待,可以这样实现一个用于selenium的等待,当然我们还可以根据项目实际情况把这个方法做一些修改,比如给参数加上一些默认值或一些日志语句:
例2. 自己封装的等待语句
from selenium import webdriver
from selenium.common.exceptions import NoSuchElementException
from robot.libraries.BuiltIn import timestr_to_secs,secs_to_timestr
import time
def wait_until_element_found(retry, retry_interval, driver, locator, param):
timeout = timestr_to_secs(retry)
retry_interval = timestr_to_secs(retry_interval)
maxtime = time.time() + timeout
while True:
try:
element = getattr(driver,locator)(param)
return element
except NoSuchElementException:
print("not found, retry..")
if time.time() > maxtime > 0:
raise AssertionError(f"Element located by {locator}({param}) failed after {retry} timeout ")
time.sleep(retry_interval)
例3.写一个会出错的等待,调试这个自己封装的等待语句
wait_until_element_found('0.5 min', '1 sec',driver, 'find_element_by_id', "kwx")
然后在百度上调用这个语句,会报错:
raise exception_class(message, screen, stacktrace)
selenium.common.exceptions.NoSuchElementException: Message: no such element: Unable to locate element: {"method":"css selector","selector":"[id="kwx"]"}
(Session info: chrome=89.0.4389.82)
During handling of the above exception, another exception occurred:
AssertionError: Element located by find_element_by_id(kwx) failed after 0.5 min timeout
三,还有一步是做“操作”。
比如要在文本框里输入文字就是 send_keys(),点击就是click(),等等。对已经定位到的元素,可以的操作加起来也就几种吧。具体selenium支持哪些操作的语法请读者自行搜索,这里就省略了。
四,当我们用selenium来做测试脚本时,还需要加入第四步骤:“断言”。这一步其实一般也就是先定位一个元素,然后判断它存在不存在。或者做取文本的操作,再判断文本符不符合预期。类似文献资料实在太多,具体写法也不赘述了。
Page Object 页面对象 与 关键字驱动 Keyword driven
一、页面对象。
许多年前,我写过一个页面对象模式的selenium框架:https://github.com/zhangting85/simpleWebtest
简单来说,页面对象的实现就是把页面上的元素封装在一个个页面类中。同时把页面上能做的操作也封装在一个个的页面类中。有时候,还会把一些常用操作封装提出来封装在一些业务层面的操作类中,称为业务方法。具体资料请搜索selenium页面对象,有许许多多的资料和实现方式。可以用page factory也可以不用。但都大同小异。
二、关键字驱动。
关键字驱动常用robot framework做。说白了,就是把selenium的语法抽象成以填表的方式来写脚本。用户在图形界面的robot ide里选择要做什么操作,输入什么字符串。也支持一些业务层面的封装。这种做法也有一些资料。
这两种模式的局限性和selenium最大的悖论
但是,在我们看过那么多页面对象和关键字驱动的资料之后,仍然会产生疑问:这两种做法真的有效吗?真的是最好的吗?
首先,我们谈谈页面对象的局限性(关键字驱动的局限性也与之类似)。
1.页面不能经常改。如果页面改了。就要更新对应的文件里的定位语句。
2.业务逻辑不能经常改。如果业务逻辑改了。就要更新对应的业务方法,还可能要更新一些测试脚本。
这导致了selenium脚本的悖论:
页面一改就跑不通,必须调通脚本之后,才能用它做测试。而调通脚本的前提是手工操作点一次,否则根本不知道改变过的页面后哪些定位语句要跟着改。因此,selenium不能发现修改过的页面的bug,如果有bug,会在更新脚本的过程中人工发现。
页面如果不改,selenium脚本还能继续跑通。但是,既然没改页面为什么要跑selenium脚本。如果是为了测业务逻辑,是不是接口测试更方便呢。
综上,selenium脚本基本没用。最终所有问题都是人工发现的。这一点,也是selenium自动化测试工程师最常遭到的挑战和质疑:不能发现bug的脚本到底有啥意义。
很多人会反驳说,回归测试脚本不是用来发现bug的,而是用来确保没有bug的。但是,它真的能确保没有bug吗?并不能。还记得selenium脚本写法里的断言吗? 断言其实一般也就是先定位一个元素,然后判断它存在不存在。或者做取文本的操作,再判断文本符不符合预期。但是,特定元素存在和文本符合预期,不代表页面就没问题:
a.页面错乱,元素位置不对等问题无法发现。
b.页面上多了不应该显示的东西是无法发现的。
这两种模式带来的调试困难
再谈一谈写页面对象脚本和关键字驱动脚本时比较头疼的另一个问题:调试困难。
一、滥用方法链导致的调试困难。
常见于页面对象模式下,我曾经也滥用过方法链,最后就是自己调试时吃苦头。
比如这样:JDHomepage().init().searchHeader.search(keyword).getProductNameByIndexMethodTwo(1);
我非常讨厌使用别人用方法链写的方法,因为中间哪一步报错了的化,要找到错误的地方太不直观了,要打个断点都不知道该打在哪里。而且要写方法链的话还必须单独写脚本来调试。调试完毕再写成方法链,只为代码看上去简洁一点。但说实话,大可不必。这是我初学编程时犯过的错误,为了简洁而写了更多更复杂的代码。表面上简洁了,实际上代码复杂度反而上升了。
而不用方法链的页面对象模式脚本,又会有很多xxxPage的对象要处理。总之两难。
二、伪代码导致的调试困难。
关键字驱动,还有它的变式,比如BDD业务驱动等等,都是通过伪代码增加了代码的复杂度。伪代码的调试也成了问题。著名关键字框架robot framework,著名BDD框架cucumber,等等,他们根本不具备断点调试功能,即使用了各种方法勉强做到断点调试,我还是想说,太麻烦了。
返璞归真
最终我的建议就是返璞归真。把测试按业务场景划分,一个业务场景一个脚本文件,全部用线性脚本实现即可。selenium就不适合用来做很完整的回归,业务逻辑交给接口测试来做,selenium能保证大部分主要页面操作过一次就好了。
有人要问,如果页面改变了怎么办。比如登陆的用户名输入框的id改了,怎么办,难道把几百个脚本里这个框的定位语句全部修改一遍吗?对啊,为什么不呢,不需要手工改,只要用IDE的replace替换功能,一次性把几百个文件里的定位语句都改了也不是难事。甚至我们还可以写python脚本来对这几百个脚本做各种各样的批量操作。比如在登陆语句前面全部插入一个新的语句。用脚本操作文本的具体做法,留待以后再讲吧。而且,为什么会出现几百个脚本里都有同一个定位语句呢?除了测登陆需要出现登陆用户名输入框以外,其他登陆的操作全都改用调接口实现了啊。
另外要注意到这样写的收益在哪里:
1.代码逻辑变得极其清晰,每个脚本文件只对自己负责,一点依赖都不存在。绝不可能别人把你的脚本改坏掉。这一点很重要,因为大量web测试都是体力劳动,修改的难度不高,可以分给很多人同时去做。而页面对象或关键字模式里,我不止一次莫名其妙地被别人提交的代码影响到。
2.脚本文件数量不少,充分体现了测试开发工程师的工作量。
3.可以更方便地进行并行执行的改造。因为脚本互相独立,让他们并行运行或分布式运行非常简单。
4.case选择非常直观,直接按照文件名来就行了。不需要搞什么tag,什么分组,再也不怕有人填错了分组名或tag名导致该跑的测试没跑了。
5.最重要的是,代码的复杂度降低了。不管是页面对象的多层抽象还是关键字驱动的伪代码,都增加了代码的复杂度。