自动化测试模型
在介绍自动化测试模型之前,我们试着来解释自动化测试库、框架和工具之间的区别。
库的英文叫做 Library,库是由代码集合成的一个产品,供程序员调用。面对对象的代码组成形成的库叫类库,面向过程的代码组织形成的库叫函数库。所以从这个角度来看,我们在第4章介绍的WebDriver就属于库的范畴,因为它提供了一组操作Web页面的类与方法,所以,我们可以称它为Web自动化测试库。
框架的英文单词叫Framework,框架是为解决一个或一类问题而开发的产品,用户一般只需要使用框架提供的类或函数,即可实现全部功能。所以从这个角度来理解 unittest 框架,它主要用于实现测试用例的组织和执行,以及测试结果的生成。因为它的主要任务就是帮助我们完成测试工作,所以我们通常把它叫做单元测试框架。
工具的英文单词叫Tools,工具与框架所做的事情类似,只是工具会有更高的抽象,屏蔽了底层的代码,一般会提供单独的操作界面供用户操作。例如,Selenium IDE 和 QTP 就是自动化测试工具。
回到自动化测试模型的概念上,自动化测试模型可以看做自动化测试框架与工具设计的思想。随着自动化测试技术的发展,演化为以下几种模型:线性测试、模块化驱动测试、数据驱动测试和关键字驱动测试。
一、自动化测试模型介绍
下面分别介绍这几种自动化测试模型的特点。
1.1 线性测试
通过录制或编写对应程序的操作步骤产生相应的线性脚本,每个测试脚本相对独立,且不产生其他依赖与调用,这也是早期自动化测试的一种形式:它们其实就是单纯的来模拟用户完整的操作场景。
这种模型的优势就是每一个脚本都是完整且独立的。所以,任何一个测试用例脚本拿出来都可以单独执行。当然,缺点也相当明显,测试用例的开发与维护成本很高:
- 开发成本很高,测试用例之间可能会存在重复的操作,不得不为每一个用例去录制或编写这些重复的操作。例如每个用例中重复的用户登录和退出操作等。
- 维护成本很高,正是因为测试用例之间存着重复的操作,所以当这些重复的操作发生改变时,就需要逐一地对它们进行修改。例如登录输入框的定位发生了改变,就需要对每一个包含登录的用例进行调整。
1.2 模块化驱动测试
正是由于线性测试的缺陷非常明显,因此早期的自动化测试专家就考虑用新的自动化测试模型来代替线性测试。做法也很简单,借鉴了编程语言中模块化的思想,把重复的操作独立成公共模块,当用例执行过程中需要用到这一模块操作时则被调用,这样就最大限度地消除了重复,从而提高测试用例的可维护性。
如下图所示,模块化的结构很好地解决了线性结构的两个问题:
- 提高了开发效率,不用重复编写相同的操作脚本。假如,已经写好一个登录模块,后续测试用例在需要登录的地方调用即可。
- 简化了维护的复杂性,假如登录按钮的等位发生了变化,那么只需修改登录模块的脚本即可,对于所有调用登录模块的测试脚本来说不需要做任何修改。
1.3 数据驱动测试
虽然模块化驱动测试很好地解决了脚本的重复问题,但是,自动化测试脚本在开发的过程中还是发现了诸多不便。例如,现在我要测试不同用户的登录,首先用的是 “张三” 的用户名登录:下一个测试用例要换成 “李四” 的用户名登录。在这种情况下,还是需要重复地编写登录脚本,因为虽然登录的步骤相同,但是登录所用的测试数据不同。
于是,数据驱动测试的概念就为解决这类问题而被提出。从它的本意来解释,就是数据的改变从而驱动自动化测试的执行,最终引起测试结果的改变。这听上去的确是个高大上的概念,而在早期的商业自动化工具中,也的确把这一概念作为一个卖点。对于数据驱动所需要的测试数据,也是通过工具内置的 Datapool 管理。
如下图所示,数据驱动说的直白点就是数据的参数化,因为输入数据的不同从而引起输出结果的不同。
不管我们读取的是定义的数组、字典、或者是外部文件(excel、csv、txt、xml等),都可以看作是数据驱动,它的目的就是实现数据与脚本的分离。
这样做的好处同样显而易见,它进一步增强了脚本的复用性。同样以登录为例,首先是重新设计登录模块,使其可以接收不同的数据,把接收到的数据作为登录操作的一部分。这样就可以很好地适应相同操作、不同数据的情况。当指定登录用户是 “张三”时,那么登录之后的结果就是 “欢迎张三”;当指定登录用户是 “李四” 时,登录结果就显示 “欢迎李四”。这就是数据驱动所希望达到的目的。
1.4 关键字驱动测试
理解了数据驱动后,无非是把 “数据” 换成 “关键字”,通过关键字的改变引起测试结果的改变。
目前市面上典型关键字驱动工具以 QTP(目前已更名为 UFT - Unified Functional Testing)、Robot Framework(RIDE)工具为主。这类工具封装了底层的代码,提供给用户独立的图形界面,以 “填表格” 的形式免除测试人员对写代码的恐惧,从而降低脚本的编写难度,我们只需使用工具所提供的关键字以 “过程式” 的方式来编写用例即可。
当然,Selenium 家族中的 Selenium IDE 也可以看做是一种传统的关键字驱动的自动化工具。
上面的脚本由 Selenium IDE 录制产生,它把每一个动作分为三部分:
- 做什么?例如打开、输入、点击等动作。
- 对谁做?通过定位方式找到要操作的对象。
- 如何做?例如输入框输入的内容为“selenium”等。
当然,关键字驱动技术也在不断发展和进步。Robot Framework 甚至可以像编程一样写测试用例。
关键字驱动也可以像写代码一样写用例,在编程的世界中,没有什么不能做;不过这样的用例同样需要较高的学习成本,与学习一门编程语言几乎相当。这样的框架越到后期越难维护,可靠性也会变差,关键字的用途与经验被局限在自己的框架内,你所学到的知识也很难用到其他地方。所以,从测试人员的经验与技术积累价值来讲,直接通过编程的方式开发自动化脚本更好。
二、模块化驱动测试实例
from selenium import webdriver driver = webdriver.Chrome() driver.implicitly_wait(10) driver.get("https://mail.qq.com/") # 登录 driver.find_element_by_id("u").clear() driver.find_element_by_id("u").send_keys("578389018@qq.com") driver.find_element_by_id("p").clear() driver.find_element_by_id("p").send_keys("********") driver.find_element_by_id("login_button").click() # 收信、写信、删除信件等操作 # ...... # 退出 driver.find_element_by_link_text("退出").click() driver.quit()
从QQ邮箱业务流程分析,邮箱所提供的功能都需要登录之后进行,例如收信、写信、删除信件等操作。对于手工来说,测试人员在执行用例的过程中可以一次登录后验证多个功能再退出,但自动化测试的执行有别于手工测试的执行,需要保持测试用例的独立性和完整性,所以每一条用例在执行时都需要登录和退出操作。这个时候就可以把登录和退出的操作封装为公共函数。当每一条用例需要登录/退出时,只需调用它们即可,从而消除代码重复,提高脚本的可维护性。
下面对登录和退出进行模块封装。
from selenium import webdriver # 登录 def login(): driver.find_element_by_id("u").clear() driver.find_element_by_id("u").send_keys("578389018@qq.com") driver.find_element_by_id("p").clear() driver.find_element_by_id("p").send_keys("*******") driver.find_element_by_id("login_button").click() # 收信、写信、删除信件等操作 # ...... # 退出 def logout(): driver.find_element_by_link_text("退出").click() driver.quit() driver = webdriver.Chrome() driver.implicitly_wait(10) driver.get("https://mail.qq.com/") login() # 调用登录模块 # 收信、写信、删除信件等操作 # ...... logout() # 调用退出模块
现在将登录的操作步骤封装到 login() 函数中,把退出的操作封装到 logout() 函数中,对于用例本身只需调用这两个函数即可,可以把更多的注意力放到用例本身的操作步骤中。
当然,如果只是把操作步骤封装成函数并没简便太多,我们需要将其放到单独的脚本文件中供其他用例调用。
# public.py class Login(object): # 登录 def user_login(self,driver): self.driver.find_element_by_id("u").clear() self.driver.find_element_by_id("u").send_keys("578389018@qq.com") self.driver.find_element_by_id("p").clear() self.driver.find_element_by_id("p").send_keys("*******") self.driver.find_element_by_id("login_button").click() # 退出 def user_logout(self,driver): self.driver.find_element_by_link_text("退出").click() self.driver.quit()
当函数被独立到单独的脚本文件中时做了一点调整,主要是为了函数增加了浏览器驱动的入参。因为函数实现的操作需要通过浏览器驱动 driver ,driver 需要通过具体调用的用例给定。
# mailTest.py
from selenium import webdriver from public import Login driver = webdriver.Firefox() driver.implicitly_wait(10) driver.get("http://mail.qq.com") # 调用登录模块 Login().user_login(driver) # 收信、写信、删除信息等操作 # ...... # 调用退出模块 Login().user_logout(driver)
首先,需要导入当前目录下 public.py 文件中的 Login() 类,在需要的位置调用类中的 user_login() 和 user_logout() 函数。这样对于每个用例的编写与维护就方便了很多。
三、数据驱动测试实例
前面提到关于数据驱动的形式有很多,我们既可以通过定义变量的方式进行参数化,也可以通过定义数组、字典的方式进行参数化,还可以通过读取文件 ( txt \ csv \ xml ) 的方式进行参数化。本节我们就通过一些例子来展示数据驱动在自动化测试中的应用。
3.1 参数化邮箱登录
同样以 QQ 邮箱登录为例,现在的需求是测试不同用户的登录。对于测试用例来说,不变的是登录的步骤,变化的是每次登录的用户名和密码,这种情况下就需要用到数据驱动方式来编写测试用例。基于前面的例子做如下修改。
# public.py ...... # 修改接口需要驱动、用户名和密码等参数 def user_login(self, driver, username, password): self.driver.find_element_by_id("u").clear() self.driver.find_element_by_id("u").send_keys("username") self.driver.find_element_by_id("p").clear() self.driver.find_element_by_id("p").send_keys("password") self.driver.find_element_by_id("login_button").click() ......
修改 user_login() 方法的入参,为其增加 username 、password 的入参,将得到的具体参数作为登录时的数据。
from selenium import webdriver from public import Login class LoginTest(object): def __init__(self, driver): self.driver = webdriver.Firefox() self.driver.implicitly_wait(10) self.driver.get("http://www.baidu.com") # admin 用户登录 def test_admin_login(self): username = "admin" password = "123" Login().user_login(self, driver, username, password) self.driver.quit() # guest 用户登录 def test_guest_login(self): username = "guest" password = "321" Login().user_login(self, driver, username, password) self.driver.quit()
# 举个例子,实际使用需要实例化 LoginTest().test_admin_login() LoginTest().test_guest_login()
创建 LoginTest 类,并在 __init__() 方法中初始化浏览器驱动、等待超时长和 URL 等。这样 test_admin_login() 与 test_guest_login() 两个测试方法只需关注登录的用户名和密码,通过调用 Login() 类的 user_login() 方法并传入具体的参数来测试不同用户的登录。
3.2 参数化搜索关键字
再来看一个百度搜索的例子。我们每天上网一般要用很多次百度搜索,而我们每次在使用百度搜素时步骤都是一样的,不一样的是每一次搜索的 “关键字” 不同。
from selenium import webdriver search_text = ["python", "中文", "text"] for text in search_text: driver = webdriver.Firefox() driver.get("http://www.baidu.com") driver.find_element_by_id("kw").send_keys(text) driver.find_element_by_id("su").click() driver.quit()
这个例子可以更充分的体现出数据驱动的概念:因为测试数据的不同从而引起测试结果的不同。
3.3 读取 txt 文件
txt 文件是我们经常操作的文件类型,Python提供了以下几种读取 txt 文件的方式。
- read(): 读取整个文件
- readline(): 读取一行数据
- readlines(): 读取所有行的数据
具体参考:Python 文件操作
3.4 读取 csv 文件
假设现在每次要读取的是一组用户数据,这一组数据包括用户名、邮箱、年龄、性别等信息,这时再使用txt文件来存放这些数据,读取起来就没那么方便了。对于这种类型的数据可以通过 CSV 文件来存放。
具体参考:Python 文件操作
3.5 读取 xml 文件
有时候我们需要读取的数据是不规则的。例如,我们需要一个配置文件来配置当前自动化测试脚本的URL、浏览器、登录的用户名和密码等,这时候就可以考虑选择使用 XML 文件来存放这些信息。
<?xml version="1.0" encoding="utf-8"?> <info> <base> <platform>Windows</platform> <browser>Firefox</browser> <url>http://www.baidu.com</url> <login username="admin" password="123456"/> <login username="guest" password="654321"/> </base> <test> <province>北京</province> <province>广东</province> <city>深圳</city> <city>珠海</city> <province>浙江</province> <city>杭州</city> </test> </info>
3.5.1 获得标签信息
from xml.dom import minidom
# 打开 xml 文档
dom = minidom.parse("test.xml")
# 得到文档元素对象
root = dom.documentElement
print(root.nodeName)
print(root.nodeValue)
print(root.nodeType)
print(root.ELEMENT_NODE)
>>>
info
None
1
1
***Repl Closed***
首先导入 xml 的minidom 模块,用来处理 XML 文件,parse() 用于打开一个XML文件,documentElement 用于得到 XML 文件的唯一根元素。
每一个节点都有它的nodeName、nodeValue、nodeType 等属性。nodeName 为节点名称;nodeValue 为节点的值,只对文本节点有效;nodeType 为节点的类型。
3.5.2 获得任意标签名
from xml.dom import minidom # 打开 xml 文档 dom = minidom.parse("test.xml") # 得到文档元素对象 root = dom.documentElement tagname = root.getElementsByTagName("browser") print(tagname[0].tagName) tagname = root.getElementsByTagName("login") print(tagname[1].tagName) tagname = root.getElementsByTagName("province") print(tagname[2].tagName) >>> browser login province ***Repl Closed***
getElementByTagName() 可以通过标签名获取标签,它所获取的对象是以数组形式存放。假如 “login” 和 “province” 标签在 text.xml 文件中有多个,则可以通过指定数组的下标的方式获取某个具体标签。
- getElementsByTagName("province) 获得的是标签名为 “province” 的一组标签
- getElementsByTagName("province) .tagname[0] 表示一组标签中的第一个
- getElementsByTagName("province) .tagname[2] 表示一组标签中的第三个
3.5.3 获得标签的属性值
from xml.dom import minidom # 打开 xml 文档 dom = minidom.parse("test.xml") # 得到文档元素对象 root = dom.documentElement logins = root.getElementsByTagName("login") # 获得 login 标签的 username 属性值 username = logins[0].getAttribute("username") print(username) # 获得 login 标签的 password 属性值 password = logins[0].getAttribute("password") print(password) # 获得第二个 login 标签的 username 属性值 username = logins[1].getAttribute("username") print(username) # 获得第二个 login 标签的 password 属性值 password = logins[1].getAttribute("password") print(password) >>> admin 123456 guest 654321 ***Repl Closed***
getAttribute() 方法用于获取元素的属性值。它和 WebDriver 中所提供的 get_attribute() 方法相似。
3.5.4 获取标签对之间的数据
from xml.dom import minidom # 打开 xml 文档 dom = minidom.parse("test.xml") # 得到文档元素对象 root = dom.documentElement provinces = dom.getElementsByTagName("province") citys = dom.getElementsByTagName("city") # 获得第二个 province 标签对的值 p2 = provinces[1].firstChild.data print(p2) # 获得第一个 city 标签对的值 c1 = citys[0].firstChild.data print(c1) # 获得第二个 city 标签对的值 c2 = citys[1].firstChild.data print(c2) >>> 广东 深圳 珠海 ***Repl Closed***
firstChild 属性返回被选节点的第一个子节点。data 表示获取该节点的数据,它和 WebDriver 中提供的 text 方法类似。