今天又是加班的一天,窗外的天空好美。——《丫子》
前言
在正式开始封装 selenium 之前,我们首先要搞清楚如下几个问题:
- 到底什么是POM,它比不用 POM好在哪里?
- OOP在POM中到底扮演什么样的角色?它为POM提供了哪些特性?
- 如果使用POM,该怎么进行规划?怎么去封装Selenium?
到底什么是POM,它比不用POM好在哪里?
对于接触过自动化测试的同学来说,大概都能说出些自己的理解。这里说一下我的理解,我打算用一个代码例子来说明这个问题,比图和文字应该更直观一些。
这次我们打算用 https://www.utest.com/ 这个网站来写测试用例,进入首页后,先打开左侧导航栏,
依次点击左侧导航栏上部的5个按钮,
然后验证其进入相应的正确页面。
假设我们根本不知道POM是什么东西,也不知道任何与测试框架有关的知识,那我们应该怎么写代码来达到测试需求呢?
我首先想到的就是按部就班地把测试步骤一步一步地实现(还在纠结于怎么定位元素,或者不知道selenium API的同学,赶紧去补习吧~),在根目录下新建Samples文件夹,并新建NoPom.py,代码如下:
而这里是Log日志:
稍微说明一下,由于反复写5遍 验证导航栏 -> 点击导航栏按钮 -> 验证title标签文本 实在是太麻烦,所以我在测试脚本中将导航栏按钮和title标签文本拼成了字典,并用for循环遍历了这个字典,从而减少了代码量。但是尽管如此,这个测试脚本依然有如下问题存在:
- 定位元素和测试步骤写在同一个测试脚本里,耦合太高。一旦元素定位发生变化,那么需要修改所有测试脚本里的元素定位方式。
- 元素是在测试脚本里定位的,无法复用。如果该元素在多个测试脚本里都需要用到,那么需要在每个测试脚本中去定位元素,代码大量冗余。
- 使用强制等待 time.sleep() 方法缺少弹性,而且稳定性也不足。在性能高的机器上会浪费测试时间,在性能低的机器上可能等待时间又不足,会出现无法定位的情况。这对于一个长期反复运行的测试脚本来说,不可取。
- 缺少测试用例组织方式,如果验证5个跳转页面的title标签文本是5条用例的话,则一个脚本无法控制精确执行哪一条测试用例。如果将这一条脚本拆分成5条脚本的话,又会因为出现前2点的问题。
- 测试结果不明显。目前的测试结果只有 log 文件,若想查看是否验证失败只能去 log 文件中进行字符串匹配才能发现是否有验证失败。效率低下而且非常不方便。
- 测试脚本过长,可读性比较差,不利于后期维护和扩展。
针对以上几点问题我们来设想解决方案: - 先说第三点,这个其实可以通过调用 selenium 的显式等待 WebDriverWait方法,再结合 until 和 until_not 方法来处理,这个稍后再说。
- 第四点,需要一个组织测试用例的工具,python3自带的unittest库就可以解决这个问题,当然也有一些其他的测试框架工具,比如 pytest,RobotFramework等,本框架打算使用最简单的unittest,基本能实现常规的测试需求,具体的实现方式稍后再说。
- 第五点,需要一个生成报告的工具,unittest有一个扩展文件可以解决这个问题,同样,我们稍后再说。
- 最后看第一和第二点。这两点其实算是同一个问题。若想解决这个问题,我们自然而然就想到了对元素定位进行封装,存放在另一个文件中,对其统一进行管理。然后在测试脚本中通过导包的方式导入这些元素。这样就可以做到一处定义,多处使用,减少了冗余代码。同时由于元素的定义是在一个独立文件中进行的,测试脚本只负责调用,所以也自然做到了解耦,一旦元素定位发生改变,只需要更改定义处的代码即可,测试脚本处完全不需要改动。
到此为止,大概能回答第一个问题“到底什么是POM,它比不用 POM好在哪里?”一半的问题了。我们知道了最傻的方式有哪些缺点,但是到底什么是POM呢?POM和基本的封装之间还有什么区别?这里我就不写代码来举例了,我们回头去看群主博客上的例子
点击跳转 ,群主是如何把最基本的定位元素,操作元素按POM模式来封装的呢?
- 首先定义一个通用的find_element(self, selector) 方法
- 然后把元素的定位方式按照固定格式写在一个页面类里
- 然后再封装selenium的click()方法,其中该方法调用了之前定义的 find_element() 方法
- 最后看看是怎么把前三步串起来的
我们不一定需要完全仿照群主的实现方式,但是需要读懂这么 实现的目的:主要是看第二步,普通封装方式会将页面元素的定位方式全存放在同一个文件里,而群主则是将其存放在相应的页面类里,后者就是POM。从代码的实现来看,POM确实要比我之前写的傻方式要简洁得多,可读性要高得多。至于POM比起普通封装方式来说要好在哪里呢,那就是第二个我们需要关注的问题。
OOP在POM中到底扮演什么样的角色?它为POM提供了哪些特性?
上文说到了POM和普通封装的区别,老规矩,我们直接上才艺:
搞错了,上代码
大家看到这里有没有一种恍然大悟的感觉?没错, 如果使用函数的形式来定义元素操作方式,发现没有办法传递driver这个变量!那么selenium的方法也就没办法调用。
可能有些同学会说,那我可以先在其他文件中生成 driver 实例,然后再在这个文件中 import 进来,一样可以用。但是这样会有一个很严重的问题——一旦driver实例化之后,再在函数中调用 driver.click() 其实就是显式调用了 click() 这个方法,driver会控制浏览器去当前页面尝试点击元素,而这里只是定义函数的地方,还没到要具体操作元素的测试脚本的范围,自然会抛出找不到元素的异常。
所以想要以函数的形式来定义,很有难度,我不知道有没有妥善的解决方案。但是这并不是个无法攻克的难题——只要你以类方法的形式来定义就可以了。
这就牵扯到OOP的一个重要特性——在代码运行时保持类中属性的状态。在定义类方法的时候,self.driver = driver这一步只是一个传递参数的过程,并没有显式的实例化 driver。而且程序一直记住了这个状态,等到真正需要调用的时候,已经实例化后的driver 才会传递给类进行页面类的初始化并调用相应的方法,类中的属性 self.driver 接收到实例化后的driver发生改变。
说了这么一大堆可能有点绕,能理解是最好,如果理解不了可以自己动手尝试一下。
除了保持类属性的状态,OOP的特性还有额外的好处,比如继承。继承是OOP最显著的特性,我就不详述了。我想说一下继承在POM中是怎么应用的。
第一点是定义一个基类class BasePage,然后把一些selenium的底层方法都封装在BasePage里,这样所有BasePage的子类都可以继承这些方法,不会有代码冗余出现。
第二点则偏向于业务,详细分析需要测试的网站,将最外层最顶层最大最通用的页面(一般都是html中比较靠上层级的元素)定义为层级较高的页面类,而一些分页,子页面,甚至页面中的大型控件(比如超复杂的Grid)定义为层级较低的页面类,低层级页面类去继承高层级页面类。这样做的好处就是可以去掉重复定义页面元素的冗余。
我们举个例子来说:
还是这个网站,左侧导航栏,很明显大家都能看出来这是网站的重要组成部分,绝大部分子页面都可以随时展开导航栏并对其进行操作。包括展开导航栏的展开按钮,也是绝大部分子页面都有的。所以我会在 直接继承BasePage类的最顶层的HomePage类定义导航栏和展开按钮,那么继承HomePage的所有子类也都会继承导航栏和展开按钮的元素定义和操作方法。
而如果某个页面,发现展开按钮的html改了,和其他页面不一致(有可能是故意设计成这样的),那么不用更改父类HomePage中的展开按钮定义方式,只需要在该页面类中重写展开按钮的定义方式就可以了。这就是OOP的另一个重要特性——多态。
利用多态这种特性,我们可以拓展并定制父类中的方法——父页面类定义通用的页面元素,子页面类根据各自的情况决定是继承,还是定制,还是在父类基础上扩展。Python3的OOP语法都支持相应的操作。
当然我在自己经历过的自动化测试项目中还意识到一个 OOP特性带来的额外好处——类本身扩展了命名空间。可能有些同学还没意识到这个问题,比如说一个巨大的网站,里面有N多个子页面,每个页面又有N多个元素(我之前经历过一个项目,页面元素写了1300多个),如何去为这些元素命名其实有时候是个很头疼的问题。比如说我那个项目里光是ok_button就有30多个。有些同学会说,这没问题啊,按页面层级来命名好了,比如homepage_vmgrid_vmnames_edit_textbox,这是个解决方案,但是说实话我见过比这个还长的变量名,当你的测试脚本中充斥着这种变量名的时候,整个人的心情都非常复杂 -_-,做代码维护的同学的热情也会降低不少。
而使用类,则可以减少这样的变量名的长度,因为不同的类中可以定义相同的变量名,就比如 homepage.okbutton 和 loginpage.okbutton ,虽然它们在定义时的名字都叫 okbutton,但在调用的时候因为在前面加上了实例调用,所以即使出现在同一个python 文件中也丝毫没有问题。
如果使用POM,该怎么进行规划?怎么去封装Selenium?
到这里为止,我们会发现POM这种模式实在是很巧妙。既然我们已经了解了它的优点,掌握了它的思想,那我们该如何去规划自己框架的模式呢?下面我会介绍我自己的理解和所采用的方式。首先要根据业务中的测试经验提取出常用的selenium AP,然后分析这些API的入参和返回值,甚至源码,使用相应的技术来进行封装。
我总结了一下,我之前 常用的API包括以下这些部分:
- Webdriver类:selenium/webdriver/remote/webdriver,所有相应的浏览器Webdriver类都继承自该类,该类封装了很多控制浏览器的方法。我们测试脚本中生成的driver对象就是该类的实例。
- WebElement 类:selenium/webdriver/remote/webelement,该类封装了很多操作网页元素的方法。element是Webdriver.find_element() 方法的返回值,同时又是WebElement类的实例对象,可以直接调用该类中的方法。我虽然看了源码,但是到现在还没搞懂,selenium是怎么做到这一步的。如果有同学知道,请不吝赐教。
- WebDriverWait 类:selenium/webdriver/support/wait,该类就是著名的显式等待,结合类中的 until() 和 until_not() 方法可以智能地去判断页面元素。
- 期望场景类:selenium/webdriver/support/expected_conditions,该类定义了各种各样的期望场景,是结合WebDriverWait的until() 和 until_not() 的利器。
- By类:selenium/webdriver/common/by,该类映射了8大元素定位方式所需的方式。如果需要将8大元素定位方法整合成一个定位方法,该类是必不可少的。
- Alert类:selenium/webdriver/common/alert,该类封装了操作网页alert的方法。Webdriver.switch_to.alert() 的返回值,同时又是Alert类的实例对象,可以调用Alert的方法,这一点有点像WebElement类的方式。
- ActionChains类:selenium/webdriver/common/action_chains,该类封装了比较底层的交互方法,比如鼠标事件,键盘事件等。该类的构造函数中接受driver参数,所以该类的方法的使用方式和 Webdriver 类中的方法类似,只不过类名不同而已。
- WebDriverException类:selenium/common/exception,该类继承自Exception类并拓展出不少子类,对应整个selenium工具的异常情况。如果我们需要在封装过程中捕获特定的异常来做处理,那么必须精确捕获某一种异常。
再结合自己遇到的在自动化测试中的应用场景,我理想中的测试脚本写起来应该包含以下几种常用的格式:
element.operation(*args, **kwargs) – 操作页面元素
page.operation(*args, **kwargs) – 操作浏览器
page.wait.something(*args, **kwargs) – 等待某些特定事件发生或不发生
page.action.something(*args, **kwargs) – 鼠标事件/键盘事件
我决定分为几部分来分别封装selenium的这些API。
- Browser类:该类负责page.operation(*args, **kwargs)部分,主要封装Webdriver类中的方法,并且定义统一的私有方法_find_element() 。届时BasePage类直接继承Browser类,也继承其中的方法。
- Element 类:该类负责element.operation(*args, **kwargs)部分,主要封装WebElement类中的方法,并且需要嵌入定位元素,智能等待过程。届时测试脚本中直接调用Element的 对象.方法() 正好满足 element.operation()的形式。
- Wait 类:该类负责page.wait.something(*args, **kwargs)部分,主要封装WebDriverWait 和expceted_conditions。由于不想每个页面都去实例化 Wait,所以利用委托技术将Wait的对象传递给page的属性 wait。
- Action类:该类负责 page.action.something(*args, **kwargs)部分,主要封装ActionChains。由于不想每个页面都去实例化 Action,所以利用委托技术将Action的对象传递给page的属性 action。
到这里为止,基本上确定了封装selenium的方式,下面我们就正式开始写代码。
End
欢迎关注公众号以及加群讨论,所有文章都会同步到公众号,方便大家在碎片时间阅读。
▲扫描二维码“识别”关注 简介:热爱生活,享受旋律!