Python UI自动化测试详解
创始人
2024-03-30 11:01:47
0

一、UI自动化测试概述

1、自动化测试的价值

传统的商业模式以业务驱动产品,而现在则以技术驱动产品。特别是在产品迭代速度快,市场不断变化的当下,产品调整,很多时候是基于客户的需求,基于整个产品战略的调整。单纯的手工测试越来越无法适应这个变化的过程。测试人员怎样做到快速响应并且保证产品在上线后质量能够满足市场的要求?怎样通过测试技术的手段来达到测试效率的提升?这些值得我们思考。

测试工作的意义,主要包括两个方面,一是产品质量的管理,二是测试效率的提升。

自动化测试技术的应用越来越普遍,这源于企业的要求和互联网技术的发展。测试人员,已经不能按过去流水线式的方式进行测试,因为这种测试效率低下的生产方式会逐渐被现代企业所淘汰。企业要在新一轮的竞争中脱颖而出,必须快速推出新的产品,并以更快的方式占领市场。测试人员需要考虑的是,怎样快速适应这种节奏的变化,更快地完成产品的测试,并且使产品质量满足发布要求。在这个过程中,可以说自动化测试技术是测试人员的必然选择,也是技术发展的选择。

站在企业管理层的角度来说,自动化测试技术能够提升团队的战斗力,降低人力成本。对于持续迭代的产品,随着产品的增加,测试人手开始显得紧张,而增加人手则意味着人力成本大幅上升。那么,通过什么样的方式来解决这个问题呢?自动化测试技术的引入可以很好地解决这个问题。在一个可持续迭代的产品测试体系中,完全可以使用自动化测试技术来实现系统已有的功能测试,即使无法达到百分之百实现,实现百分之五十也是很好的,例如,测试环境合并代码后的回归测试,上线后的冒烟测试[1]等。而测试人员只需要测试新的功能点和自动化测试未实现的功能点,这样就大大地提高了整个测试的效率。

2、自动化测试的应用

测试人员需要思考如何提升测试效率,如何让自动化测试技术应用在产品测试中,从而给自己留出更多的时间来思考产品质量策略和新的测试场景。在笔者看来,不管是个人还是团队,既然考虑引进自动化测试的技术,就要把它应用起来。笔者也曾经见过有些团队把产品自动化测试做起来后,由于维护成本大又放弃了。其实,任何技术,都既存在优点也存在缺陷,没有一个测试框架能够解决所有的问题,我们应该把它应用到合适的场景中,让它带给我们想要的效果。

1)UI自动化测试的应用

对于产品迭代速度很快的公司,特别是互联网公司,使用 UI 自动化测试技术并不是一个很理想的选择。这主要是由于互联网产品迭代速度太快,使用 UI自动化就会陷入不断的维护之中。在产品上线前,用 UI 自动化测试用例进行测试的执行时间很长,导致人员必须等待。所以针对这种类型的产品测试,可以使用UI自动化测试只覆盖主要流程的方法,只对产品的核心流程进行UI自动化测试,而不覆盖大多数测试点。

针对迭代速度不是很快、开发周期比较长的产品,使用 UI 自动化测试是一种比较好的选择,它可以把能够覆盖到的场景都覆盖到。在实际的测试过程中,如果在晚上下班的时候自动执行 UI 自动化测试用例,则第二天早上就可以看到自动化测试的执行结果,依据自动化测试报告信息,就能够得到本次迭代产品的质量情况。在下次迭代中依此循环,直到产品(项目)测试结束。

2)接口自动化测试的应用

前面提到,当产品迭代速度很快时,使用UI自动化测试的技术并不是一个理想的选择。在测试的金字塔模型中,第一层是 UI层,第二层是 API层,第三层是UNIT层。当产品迭代速度快时,选择API层进行测试是一种比较好的方案。

另外,基于目前,的开发模式基本都是前后端分离的,做接口测试也是一种比较好的选择。选择接口测试技术还有一个优势是,接口相对来说比较稳定,因此测试的效率比较高。测试时应该把更多的精力和时间放在客户端与服务端之间的交互上,放在前后端分离的模式中。平常我们所测试到的产品(WEB、APP、PAD 等类型)都是客户端产品,这样我们只需要更多地关注客户端与服务端的业务交互和逻辑处理。测试接口时,不能仅仅测试客户端与服务端之间的通信,这样完全不能满足测试的需求。在接口测试时,有两个维度是必须要考虑的。

第一是 API 的内部逻辑。这主要包含异常处理的测试。通过测试来验证服务端程序的稳定性、健壮性及容错性,测试点的选择需要考虑的主要是请求参数的字段类型、字段参数中不能为空的字段验证,以及请求参数边界值的验证。

第二是接口功能的完整性。通俗地说,是通过接口测试技术来测试产品的业务逻辑和流程。也就是说,首先要保证产品的业务流程是好的,然后才考虑产品的性能测试和安全测试。可以使用主流的测试工具(PostMan,JMeter),或者 Python语言代码来进行产品的接口自动化测试。这样,不管是在测试阶段还是在上线阶段后的冒烟测试,接口测试的执行速度是非常快的,即使几百个的接口测试用例,也只需要几分钟就会执行完毕。

微服务目前已不是一件很新鲜的事了,很多公司目前都在应用。我认为它逐步会发展成测试的主要趋势。在微服务中,以一组 API 提供业务功能的组件;微服务之间是以HTTP等轻量级的通信方式进行调用。那么针对微服务的测试,需要做的就是了解微服务的内部请求方式,然后通过应用层的协议来测试它。当然这中间还涉及契约测试。

测试需要保障每个微服务组件的内部功能是正确的,同时还要保障每个微服务组件之间连接的正确性。微服务测试还需要注意测试微服务提供的功能是否能够满足他人的需求,例如,开放平台的微服务能否满足第三方的调用和业务需求。

二、Selenium元素定位

1、Selenium简介

Selenium 是一个用于Web 应用程序的自动化测试工具。Selenium 直接运行在浏览器中,它可以模拟用户的行为操作,操作界面友好。Selenium 支持 IE、Google Chrome、Firefox、Opera等主流浏览器,同时Selenium也支持主流开发语言,如 Java、Python、C#等。

目前,一些主流浏览器厂商已经采取措施使Selenium成为浏览器的一部分,厂商还提供了不同的驱动程序(Driver)来兼容 Selenium的版本。目的是使浏览器在执行程序时更加稳定。

其具有以下特性:

  1. 开源:可以根据需要来增加或者重构工具的某些功能。
  2. 跨平台: Linux、Windows、Mac。
  3. 支持多种编程语言。
  4. 核心功能就是可以在多个浏览器上进行自动化测试。
  5. 目前已经被 google、百度、腾讯等公司广泛使用。

2、Selenium发展历程

1. Selenium 1.0

2004 年,ThoughtWorks 公司里一个叫做 Jason Huggins 为了减少手工测试的工作,自己写了一套 JavaScript 的库,这套库可以进行页面交互,并且可以重复的在不同浏览器上进行重复的测试操作。

这套库后来变为了 Selenium Core,为 Selenium Remote Control(RC)和 Selenium IDE 提供了坚实的核心基础能力。Selenium 1.x 时期主要使用 Selenium RC(Selenium Remote Control)进来自动化测试。

Selenium 的作用是划时代的,因为他允许你使用多种语言来控制浏览器。

Selenium 1.0 组成:

  1. Selenium IDE:Selenium IDE 是嵌入到 Firefox 浏览器中的一个插件,实现简单的浏览器操作的录制与回放功能。

  2. Selenium Remote Control(RC):Selenium 家族的核心部分。Selenium RC 支持多种不同语言编写的自动化测试脚本,通过 Selenium RC 的服务器作为代理服务器去访问应用,从而达到测试的目的。Selenium RC 包含 Selenium Core、Client Libraries 和 Selenium Server 三个部分:
    1. Client Libraries主要用于编写测试脚本,用来控制 Selenium Server 的库。
    2. Selenium Server:负责控制浏览器行为。
    3. Selenium Core:是整个测试机制的核心部分,即有 assertion(断言)机制的 test suite runner。它由一些纯 js 代码组成,可以运行在 Windows/Linux 的不同 Browser 上。
  3. Selenium Grid:可以并行的在多个测试环境执行测试脚本,实现脚本的并发测试执行,以此缩短大量测试脚本集合的执行时间。

Selenium RC 工作原理:

  • Selenium Server 负责控制浏览器行为,总的来说,Selenium Server 主要包括3个部分:Launcher、Http Proxy、Selenium Core。其中 Selenium Core 是被 Selenium Server 注入到浏览器页面中的,它其实就是一堆 Javascript 函数的集合。
  • 自动化测试的过程是:Selenium RC 启动一个 Selenium Server,将操作 web 元素的 API 调用转化为一段段 Javascript,在 Selenium 内核启动浏览器之后注入这段 Javascript 函数(即Selenium Core),通过这些 Javascript 函数,我们才可以实现用程序对浏览器进行操作(Javascript 可以获取并调用页面的任何元素,自如的进行操作)。
  • Client Libraries 是写测试用例时用来控制 Selenium Server 的库。测试用例通过调用 Client Libraries 来编写相关的代码。

Selenium 1.0 工作原理:

  1. 测试用例(TestCase)通过 Client Libraries 的接口向 Selenium Server 发送 Http 请求,要求和 Selenium Server 建立连接。
  2. Selenium Server 的 Launch 启动浏览器,把 Selenium Core 加载入浏览器页面中,并将浏览器的代理设置为 Selenium Server 的 Http Proxy 。
  3. 测试用例通过 Client Libraries 的接口向 Selenium Server 发送 Http 请求,Selenium Server 对请求进行解析,然后通过 Http Proxy 发送 JS 命令通知 Selenium Core 执行操作浏览器的动作。
  4. Selenium Core 接收到指令后,执行操作。
  5. 浏览器收到新的页面请求信息,于是发送 Http 请求,请求新的 web 页面。Selenium Server 会接收到所有由它启动的浏览器发动的请求。
  6. Selenium Server 接收到浏览器发送的 Http 请求后,自己重组 Http 请求,获取对应的 web 页面(响应)。
  7. Selenium Server 的 Http Proxy 把接收的 Web 页面返回给浏览器。

2. Selenium 2(WebDriver)

Selenium RC 的缺点:

  • Selenium RC 不能处理本机键盘和鼠标事件。
  • Selenium RC 不能处理弹出框、对话框(基本身份认证、文件上传/下载)事件。
  • Selenium RC 使用 Javascript 注入技术,速度不够理想,稳定性大大依赖于 Selenium 内核对 API 翻译成的 Javascript 质量高低。

同时,Web 应用也越来越强大,特性也越来越多,都对 Selenium 的发展来说带来了不少困难。

2006 年,Google 的工程师 SimonStewart 发起了 WebDriver 的项目;因为长期以来 Google 一直是 Selenium 的重度用户,但却被限制在有限的操作范围内,且 WebDriver 项目的目标就是为了解决 Selenium 的痛处。

2008 年(北京奥运会年), Selenium 和 WebDriver 这两个项目进行了合并,Selenium 2.0 出现了,也就是大家常说的 WebDriver 。

Selenium 与 WebDriver 原是属于两个不同的项目,WebDriver 的创建者 Simon Stewart 早在 2009 年 8 月的一份邮件中解释了项目合并的原因:

  • 部分原因是 WebDriver 解决了 Selenium 存在的缺点(例如能够绕过 JavaScript 沙箱,我们有出色的 API)。
  • 部分原因是 Selenium 解决了 WebDriver 存在的问题(例如支持广泛的浏览器)。
  • 部分原因是 Selenium 的主要贡献者和我都觉得合并项目是为用户提供最优秀框架的最佳途径。

一直以来,Selenium RC 是在浏览器中运行 JavaScript 应用,使用浏览器内置的 JavaScript 翻译器来翻译和执行 Selenese 命令(Selenese 是 Selenium 命令集合)。

而 WebDriver 提供了另外一种方式与浏览器进行交互。那就是利用浏览器原生的 API,封装成一套更加面向对象的 Selenium WebDriver API,可以直接操作浏览器页面里的元素,甚至操作浏览器本身(截屏,窗口大小,启动,关闭,安装插件,配置证书之类的)。由于使用的是浏览器的原生 API,速度大大提高,而且调用的稳定性交给了浏览器厂商本身,显然是更加科学。同时也避免了 JavaScript 安全模型(绕过 JS 环境的沙盒效应)导致的限制。

然而带来的一些副作用就是,由于不同的浏览器厂商对 Web 元素的操作和呈现存在不同程度的差异,这就要求 Selenium WebDriver 要分浏览器厂商的不同,提供不同的实现。例如 Chrome 有专门的 ChromeDriver,Firefox 有 FirefoxDriver 等等。

除了来自浏览器厂商的支持之外,WebDriver 还可以利用操作系统级的调用,模拟用户输入。

WebDriver工作原理:

  1. 对于每一条 Selenium 脚本,都会创建一个 HTTP 请求并发送给浏览器的驱动。Selenium 首先会确认浏览器的 native component 是否可用而且版本匹配。若匹配则在目标浏览器里启动一整套 Web Service。这套 Web Service 使用了 Selenium 自己设计定义的协议——The WebDriver Wire Protocol。这套协议非常之强大,几乎可以操作浏览器做任何事情,包括打开、关闭、最大化、最小化、元素定位、元素点击、文件上传等等。
  2. 浏览器驱动中包含了一个 HTTP Server(Remote Server),用来接收这些 HTTP 请求。发送请求时,用 WebDriver 的 HttpCommandExecutor 类将命令转换为 URL 作为 Value,命令作为 Key 一起存入 Map 作为 Request,同时会在 Request 的 Body 中存放相应的 By Xpath、id、name。实际发送的 URL 都是相对路径,后缀多以 /session/:sessionId 开头,这也意味着 WebDriver 每次启动浏览器都会分配一个独立的 sessionId,多线程并行的时候彼此之间不会有冲突和干扰。
    • 比如我们要访问某一个网站,则请求地址为 http://localhost:46350/wd/hub/session/sessionId/url,请求 Json 内容为 {"url": "http://www.qq.com"}。
    • 比如我们常用到的 find_element_by_class_name 这个接口,则会转化为 /session/:sessionId/element 这个 URL,然后在发出 Http Request Body 内再附上具体的参数(如 class name 的值)。如要查找一个 classname 为 test 的元素,则请求地址后缀为 /session/sessionId/element,请求 Json 内容为 {"using": "class_name", "value": "test"}。
  3. HTTP Server 接收到请求后根据请求来具体操控对应的浏览器。
  4. 浏览器执行具体的测试步骤。
  5. 浏览器将步骤执行结果返回给 HTTP Server。
  6. HTTP Server 又将结果返回给 Selenium 的脚本,响应内容也是 Json,会返回找到的 element 的各种细节,比如 text、CSS selector、tag name、class name 等等。比如:
{"sessionId": "XXXXX", "status": 0, "state": "success", "value": {"ELEMENT":"2"}, "class": "XXX", "hCode": "XXX"}

JSON Wire protocol

WebDriver 的 JSON Wire 协议是通用的,也就是说不管是 FirefoxDriver 还是 ChromeDriver,启动之后都会在某一个端口启动基于这套协议的 Web Service。例如 ChromeDriver 初始化成功之后,默认会从 http://localhost:46350 开始,而 FirefoxDriver 从 http://localhost:7055 开始。后续我们调用 WebDriver 的任何 API,都需要借助一个 ComandExecutor 发送一个命令,实际上是一个 HTTP Request 给监听端口上的 Web Service。在我们的 HTTP Request 的 Body 中,会以 WebDriver Wire 协议规定的 JSON 格式的字符串来告诉 Selenium 我们希望浏览器接下来做什么事情。

JSON Wire protocol 是在 HTTP 协议基础上,是对 HTTP 请求及响应的 Body 数据的进一步规范。

为什么要基于 HTTP 协议呢?因为 HTTP 协议是一个浏览器和 Web 服务器之间通信的标准协议,而几乎每一种编程语言都提供了丰富的 http libraries,这样就可以方便的处理客户端 Client 和服务器 Server 之间的 Request 及 Response,WebDriver 的结构就是典型的 C/S 结构,WebDriver API 相当于是客户端,而小小的浏览器驱动才是服务器端。

在 WebDriver 中为了给用户以更明确的反馈信息,提供了更细化的 HTTP 响应状态码,比如:

7: NoSuchElement
11:ElementNotVisible
200:Everything OK
……

Body 部分主要传送具体的数据,在 WebDriver 中这些数据都是以 JSON 的形式存在并进行传送的,这就是 JSON Wire protocol。

JSON 是一种数据交换的格式,是对 XML 的升级与替代。下面的例子是 WebDriver 中在成功找到一个元素后 JSON Wire Protocol 的返回:

{"status" : 0, "value" : {"element" : "123422"}}

所以在 Client 和 Server 之间,只要是基于 JSON Wire Protocol 来传递数据,就与具体的脚本语言无关了,这样同一个浏览器的驱动就即可以处理 Java 语言的脚本,也可以处理 Python 语言的脚本了。

需要强调的是,在 Selenium 2.0 中主推的是 WebDriver,可以将其看作 Selenium RC 的替代品(Selenium 为了保持向下的兼容性,所以在 Selenium 2.0 中并没有彻底地抛弃Selenium RC)。

3. Selenium 3.0

2016 年 7 月,Selenium 3.0 悄悄发布第一个 beta 版。Selenium 3.0 与 Selenium 2.0 最根本的区别并不是太大,最主要的变化如下:

  1. 去掉了对 Selenium RC 的支持。
  2. 只支持 Java 8 及以上版本。
  3. 对 Driver 方面的改动,增加了对 Safari 和 Edge 浏览器的支持;同时不再提供默认浏览器支持,所有支持的浏览器均由浏览器官方提供相应的 Driver 驱动进行支持,从而提高了自动化测试的稳定性。

WebDriver 协议如今已经成为各浏览器提供商共同支持的官方标准,也成了业内公认的浏览器 UI 测试的标准实现,因此 Selenium 3.0 的核心仍然是 WebDriver。

重要意义:

  1. WebDriver 协议现在已经成为业内公认的浏览器 UI 测试的标准实现,该协议是 Google 对开源测试领域的重要贡献。
  2. 各种官方支持,意味着以后浏览器 UI 测试的速度和稳定性会有较大的提升。
  3. 浏览器 UI 自动化测试已经成为了行业标配。
  4. Selenium 专注 Web 测试。

3、xpath元素定位

1. 由父节点定位子节点

最简单的肯定就是由父节点定位子节点了,我们有很多方法可以定位,下面上个例子:

想要根据 B节点 定位无id的子节点,代码示例如下: 

第1到第3都是我们熟悉的方法,便不再多言。第4种方法用到了css选择器:nth-child(n),该选择器返回第n个节点,该节点为div标签;第5种方法用到了另一个css选择器: nth-of-type(n),该选择器返回第n个div标签,注意与上一个选择器的区别;第6种方法用到了xpath轴 child,这个是xpath默认的轴,可以忽略不写,其实质是跟方法2一样的。

当然,css中还有一些选择器是可以选择父子关系的如last-child、nth-last-child等,感兴趣可以自行百度,有机会博主会讲讲css selector。

2. 由子节点定位父节点

由子节点想要定位到父节点就有点难度了,对应以下html代码:

我们想要由 C节点 定位其两层父节点的div,示例代码如下: 

这里我们有两种办法,第1种是 .. 的形式,就像我们知道的,. 表示当前节点,.. 表示父节点;第2种办法跟上面一样,是xpath轴中的一个:parent,取当前节点的父节点。这里也是css selector的一个痛点,因为css的设计不允许有能够获取父节点的办法(至少目前没有)

3. 由弟弟节点定位哥哥节点

这是第3、第4种情况,我们这里要定位的是兄弟节点了。如以下html:

怎么通过 D节点 定位其哥哥节点呢?看代码示例: 

这里列举了两种方法,一种是通过该节点的父节点来获得哥哥节点,另外一种比较优雅,是通过 xpath轴:preceding-sibling,其能够获取当前节点的所有同级哥哥节点,注意括号里的标号,1 代表着离当前节点最近的一个哥哥节点,数字越大表示离当前节点越远,当然,xpath轴:preceding也可以,但是使用起来比较复杂,它获取到的是该节点之前的所有非祖先节点 

4. 由哥哥节点定位弟弟节点

html 与 3 一致,要想通过 D节点 定位其弟弟节点,看代码示例:

4、Selenium3 常用 API 

1. 元素定位

''' 使用id定位 '''
driver.find_element_by_id("id值")
driver.find_element("id", "id值")  # 该方式可分离定位方式,更适合封装''' 使用xpath定位 '''
# 定位单个元素
driver.find_element_by_xpath("xpath定位表达式")
driver.find_element("xpath", "xpath定位表达式")
# 定位多个元素,返回列表
driver.find_elements_by_xpath("xpath定位表达式")
driver.find_elements("xpath", "xpath定位表达式")''' 使用name定位 '''
# 定位单个元素
driver.find_element_by_name("name值")
driver.find_element("name", "name值")
# 定位多个元素
driver.find_elements_by_name("name值")
driver.find_elements("name", "name值")''' 使用classname定位 '''
# 定位单个元素
driver.find_element_by_class_name("class属性值")
driver.find_element("classname", "class属性值")
# 定位多个元素
driver.find_elements_by_class_name("class属性值")
driver.find_elements("classname", "class属性值")''' 使用标签名称定位 '''
# 定位单个元素
driver.find_element_by_tag_name("标签名称")
driver.find_element("tagname", "标签名称")
# 定位多个元素
driver.find_elements_by_tag_name("标签名称")
driver.find_elements("tagname", "标签名称")''' 使用链接的全部文字定位 '''
# 定位单个元素
driver.find_element_by_link_text("链接全部本文内容")
driver.find_element("linktext", "链接全部本文内容")
# 定位多个元素
driver.find_elements_by_link_text("链接全部本文内容")
driver.find_elements("linktext", "链接全部本文内容")''' 使用部分链接文字定位 '''
# 定位单个元素
driver.find_element_by_partial_link_text("链接的部分本文内容")
driver.find_element("partial link text", "链接的部分本文内容")
# 定位多个元素
driver.find_elements_by_partial_link_text("链接的部分本文内容")
driver.find_elements("partial link text", "链接的部分本文内容")''' 使用CSS方式定位 '''
# 定位单个元素
driver.find_element_by_css_selector("CSS定位表达式")
driver.find_element("css selector", "CSS定位表达式")
# 定位多个元素
driver.find_elements_by_css_selector("CSS定位表达式")
driver.find_elements("css selector", "CSS定位表达式")

2. 获取页面元素的属性信息

from selenium import webdriver
import timedriver = webdriver.Chrome()
driver.get("https://baidu.com")searchBox = driver.find_element_by_id('kw')
element = driver.find_element_by_xpath("//a[text()='使用百度前必读']")# 元素的标签名
print(element.tag_name)
# 元素的大小
print(element.size)
# 元素的本文内容
print(element.text)
# 获取元素指定属性的值,等价于get_property())
print(element.get_attribute("name"))  # 如"name"、"value"等属性element = driver.find_element_by_id("kw")
element.send_keys("hippop")
print(element.get_attribute("value"))  # 获取输入框的值# 获取页面元素的CSS属性值
searchBox = driver.find_element_by_id('kw')
print(searchBox.value_of_css_property("height"))
print(searchBox.value_of_css_property("width"))button = driver.find_element_by_id('su')
print(button.value_of_css_property("font-size"))
print(button.value_of_css_property("font-family"))time.sleep(3)
# driver.close()  # 关闭浏览器的当前tab窗口
driver.quit()  # 关闭浏览器

3. 元素判断

1)判断元素是否存在

from selenium import webdriver
from selenium.webdriver import ActionChains
import timedriver = webdriver.Chrome()
driver.get("http://www.baidu.com")# 函数封装:判断元素是否存在
def isElementPresent(driver, by, value):try:element = driver.find_element(by=by, value=value)except NoSuchElementException as e:# 打印异常信息print(e)# 发生了NoSuchElementException异常,说明页面中未找到该元素,返回Falsereturn Falseelse:# 没有发生异常,表示在页面中找到了该元素,返回Truereturn Trueif isElementPresent(driver, "id", "kw"):driver.find_element_by_id("kw").send_keys("hiphop")time.sleep(3)
driver.quit()

2)element.is_displayed():判断元素是否可显示(未被隐藏)

from selenium import webdriver
import timedriver = webdriver.Chrome()
driver.get("http://39.100.104.214/test_visible.html")element = driver.find_element_by_id("div1")
print(element.is_displayed())  # True
element = driver.find_element_by_id("div2")
print(element.is_displayed())  # Falsetime.sleep(3)  # 休眠3秒后点击按钮,让元素隐藏
button = driver.find_element_by_id("button1")
button.click()element = driver.find_element_by_id("div1")
print(element.is_displayed())  # False
element = driver.find_element_by_id("div2")
print(element.is_displayed())  # Truetime.sleep(2)
driver.quit()

3)element.is_enabled():判断元素是否可用

from selenium import webdriver
import timedriver = webdriver.Chrome()
driver.get("http://39.100.104.214/test_enable.html")button = driver.find_element_by_id("input1")  # 可操作
print(button.is_enabled())  # Truebutton = driver.find_element_by_id("input2")  # 不可用(标签中加了"disabled"属性)
print(button.is_enabled())  # Falsebutton = driver.find_element_by_id("input3")  # 只读(标签中加了"readonly"属性)
print(button.is_enabled())  # Truetime.sleep(2)
driver.quit()

4)element_located_selection_state_to_be():判断一个元素的状态是否是给定的选择状态

方式 1:element_selection_state_to_be(driverObject, state)

  • driverObject:定位器。
  • state:期望的元素状态,True 表示选中状态,False 反之。相等返回 True,否则返回 False。
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC# 设置显式等待时间为10秒
wait = WebDriverWait(driver, 10)# 判断给定的元素是否被选中
# EC.element_selection_state_to_be(driverObject, state)# 设定元素当前选中状态为True
EC.element_selection_state_to_be(driver.find_element_by_id("peach"), True).is_selected  # 此句会先校验元素是否存在,不存在则抛异常# 显式等待元素是否为选中状态,若是选中状态则返回True,若超过10秒仍不为选中状态,则跑出异常(异常提示信息为:超时10秒)
wait.until(EC.element_selection_state_to_be(driver.find_element_by_id("peach"), True))
# 显式等待元素是否不为选中状态,若不为选中状态则返回False
wait.until_not(EC.element_selection_state_to_be(driver.find_element_by_id("peach"), True))

方式 2:element_selection_state_to_be(locator, state)

  • locator:定位器,是一个元组 (by, path)。
  • state:期望的元素状态,True 表示选中状态,False 反之。相等返回 True,否则返回F alse。
1 # 原型
2 EC.element_located_selection_state_to_be((By.ID, "peach"), True).is_selected  # 不会校验元素是否存在,返回True
3 # True
4 wait.until(EC.element_located_selection_state_to_be((By.ID, "peach"), True))
5 # Flase
6 wait.until_not(EC.element_located_selection_state_to_be((By.ID, "waterlemon"), True))

5)其他

  • element_to_be_clickable(locator):判断某元素是否可见且能被单击,满足则返回元素对象,否则返回 False
1 # 存在并可见
2 wait.until(EC.element_to_be_clickable((By.ID, "")))
3 # 不存在
4 wait.until_not(EC.element_to_be_clickable((By.ID, "")))
  • frame_to_be_available_and_switch_to_it(parm):判断 frame 是否可用,如果可用返回 True 并切入到该 frame

参数 parm 可以是:

  1. 定位器 locator
  2. 定位方式(id、name等)
  3. 元素对象
  4. frame 在页面中的索引值
  • invisibility_of_element_located(locator):希望某元素不出现在页面的 DOM 中
  • visibility_of_element_located(locator):希望某元素出现在页面的 DOM 中
1 element = wait.until(EC.visibility_of_element_located((By.ID, ""), True))
2 
3 element = EC.visibility_of(driver.find_element_by_id(""))
4 
5 # 判断页面上至少一个元素可见,返回满足条件的所有页面对象。
6 inputs = wait.until(EC.visibility_of_any_elements_located((By.TAG_NAME, "input")))
  • presence_of_all_elements_located(locator):判断页面上至少一个元素出现(不一定可见),返回满足条件的所有页面对象
1 elements = wait.until(EC.presence_of_all_elements_located((By.TAG_NAME, "input")))
2 # 判断单个元素出现(不一定可见)
3 element = wait.until(EC.presence_of_element_located((By.TAG_NAME, "input")))
  • staleness_of(driver.find_element_by_id(""):判断元素是否仍在 DOM 中,如果在规定时间内已经移除,返回 True,反之 False
  • text_to_be_present_in_element((By.ID, ""):判断文本内容 text 是否出现在某个元素中,判断的是元素的 text
  • text_to_be_present_in_element_value(locator, text):判断文本内容 text 是否出现在某个元素的 value 属性值中

4. 元素操作

1)操作输入框

  • 输入内容:element.send_keys()

  • 清空内容:element.clear()

单击:

element.click()

from selenium import webdriver
import timedriver = webdriver.Chrome()
driver.get("https://baidu.com")
time.sleep(2)searchBox = driver.find_element_by_id('kw')
searchBox.send_keys("before")time.sleep(2)# 为了防止缓存的文字内容干扰测试结果,把输入框的内容先清空
searchBox.clear()
searchBox.send_keys("after")# 定位"百度一下"按钮
button = driver.find_element_by_id("su")
# 单击按钮
button.click()time.sleep(2)
driver.quit()

双击:

action_chains.double_click(element).perform()

from selenium import webdriver
from selenium.webdriver import ActionChains
import timedriver = webdriver.Chrome()
driver.get("http://39.100.104.214/test_doubleclick.html")
time.sleep(2)element = driver.find_element_by_id("inputBox")action_chains = ActionChains(driver)
# 双击元素
action_chains.double_click(element).perform()time.sleep(2)
driver.quit()

2)下拉框操作

单选:

from selenium import webdriver
from selenium.webdriver.support.ui import Select
import timedriver = webdriver.Chrome()
driver.get("http://39.100.104.214/test_select.html")# 获取下拉框对象
select_element = Select(driver.find_element_by_xpath("//select"))
print(select_element)# 打印默认选中的选项的文本
print(select_element.first_selected_option.text)# 获取所有的选项对象
all_options = select_element.options
print(all_options)# 判断若第二个选项对象可用,并且没有被选中,就选中它
if all_options[1].is_enabled() and not all_options[1].is_selected():# 方式1:通过索引来选择选项select_element.select_by_index(1)print (select_element.all_selected_options[0].text)assert "西瓜"  in select_element.all_selected_options[0].texttime.sleep(1)
# 方式2:通过文本来选择选项
select_element.select_by_visible_text("猕猴桃")
assert "猕猴桃" in select_element.all_selected_options[0].texttime.sleep(1)
# 方式3:通过value属性值来选择选项
select_element.select_by_value("shanzha")
assert "山楂" in select_element.all_selected_options[0].texttime.sleep(2)
driver.quit()

封装下拉框操作:

from selenium import webdriver
from selenium.webdriver.support.ui import Select
import time# 根据xpath定位到下拉框对象,再根据选项的文本值选中指定选项
def choose_option(driver, select_xpath, option_text):select = driver.find_element_by_xpath(select_xpath)all_options = select.find_elements_by_tag_name("option")# 遍历所有选项对象for option in all_options:# print ("选项显示的文本:", option.text)# print ("选项值为:", option.get_attribute("value"))# 如果选项的文本值为入参,则选中if option.text == option_text:option.click()returndriver = webdriver.Chrome()
driver.get("http://39.100.104.214/test_select.html")# 找到页面上name属性为“fruit”的下拉框对象,并选中"猕猴桃"选项
choose_option(driver, "//select[@name='fruit']", "猕猴桃")time.sleep(2)
driver.quit()

校验下拉框中的全部选项内容:

from selenium import webdriver
from selenium.webdriver.support.ui import Select
import timedriver = webdriver.Chrome()
driver.get("http://39.100.104.214/test_select.html")# 定位下拉框对象
select_element = Select(driver.find_element_by_xpath("//select"))
# 获取所有选项对象
actual_options = select_element.options
# 以列表形式获取所有选项的文本值
actual_options_text = [option.text for option in actual_options]
print(actual_options_text)expected_values = ['桃子', '西瓜', '橘子', '猕猴桃', '山楂', '荔枝']
# 断言下拉框的选项内容是否全部相等
assert actual_options_text == expected_valuestime.sleep(2)
driver.quit()

多选:

from selenium import webdriver
from selenium.webdriver.support.ui import Select
import timedriver = webdriver.Chrome()
driver.get("http://39.100.104.214/test_multiple_select.html")select_element = Select(driver.find_element_by_xpath("//select"))
actual_options = select_element.options# 和单选的下拉框一样,遍历所有的选项文字和value
for option in actual_options:print(option.text)print(option.get_attribute("value"))if option.text == "桃子":  # 基于选项文本选择option.click()if option.get_attribute("value") == "lizhi":  # 基于value选择option.click()# 打印所有被选中option的值
print("First choices:")
for option in select_element.all_selected_options:print (option.text)time.sleep(2)
select_element.select_by_index(1)  # 基于坐标选择
select_element.select_by_visible_text("荔枝")  # 基于选项文本选择
select_element.select_by_value("juzi")  # 基于value选择
select_element.deselect_all()  # 取消所有的选项# 打印所有被选中option的值
print("Second choices:")
for option in select_element.all_selected_options:  # 空print (option.text)time.sleep(2)
select_element.select_by_index(1)
select_element.select_by_visible_text("荔枝")
select_element.select_by_value("juzi")time.sleep(2)
select_element.deselect_by_visible_text("荔枝")  # 基于选项文本取消选择
select_element.deselect_by_index(1)  # 基于坐标取消选择
select_element.deselect_by_value("juzi")  # 基于value取消选择# 打印所有被选中option的值
print("Third choices:")
for option in select_element.all_selected_options:  # 空print (option.text)time.sleep(2)
driver.quit()

3)键盘操作

selenium 键盘操作:

from selenium import webdriver
from selenium.webdriver.common.keys import Keys
import timedriver = webdriver.Chrome()
driver.get("http://www.baidu.com")driver.find_element_by_id("kw").clear()  # 清空输入框driver.find_element_by_id("kw").send_keys("hiphop")  # 输入框内写入文本
driver.find_element_by_id("kw").send_keys(Keys.ENTER)  # 模拟键盘回车操作time.sleep(5)
driver.quit()

系统键盘事件封装:

from selenium import webdriver
from selenium.webdriver import ActionChains
from selenium.webdriver.common.keys import Keys
import win32clipboard as w
import win32con
import time
import win32api# 读取剪切板
def getText():w.OpenClipboard()d = w.GetClipboardData(win32con.CF_TEXT)w.CloseClipboard()return d# 设置剪切板内容
def setText(aString):w.OpenClipboard()w.EmptyClipboard()w.SetClipboardData(win32con.CF_UNICODETEXT, aString)w.CloseClipboard()VK_CODE ={'enter':0x0D,'ctrl':0x11,'a':0x41,'v':0x56,'x':0x58}# 键盘键按下
def keyDown(keyName):win32api.keybd_event(VK_CODE[keyName], 0, 0, 0)# 键盘键抬起
def keyUp(keyName):win32api.keybd_event(VK_CODE[keyName], 0, win32con.KEYEVENTF_KEYUP, 0)driver = webdriver.Chrome()
url = "https://www.baidu.com"
driver.get(url)
content = '软件测试'setText(content)  # 设置剪切板中文字内容为:'软件测试'
getContent = getText()  # 获取剪切板中的内容
print ("剪切板中的内容:", getContent.decode("gbk"))
driver.find_element_by_id("kw").click()  # 点击输入框,使焦点保持在输入框中time.sleep(1)
keyDown('ctrl')
keyDown('v')
# 释放 ctrl + v 组合键软件测试
keyUp('v')
keyUp('ctrl')driver.find_element_by_id('su').click()  # 点击搜索按钮time.sleep(3)
driver.quit()#所有的键盘按钮定义
'''
VK_CODE = {'backspace': 0x08,'tab': 0x09,'clear': 0x0C,'enter': 0x0D,'shift': 0x10,'ctrl': 0x11,'alt': 0x12,'pause': 0x13,'caps_lock': 0x14,'esc': 0x1B,'spacebar': 0x20,'page_up': 0x21,'page_down': 0x22,'end': 0x23,'home': 0x24,'left_arrow': 0x25,'up_arrow': 0x26,'right_arrow': 0x27,'down_arrow': 0x28,'select': 0x29,'print': 0x2A,'execute': 0x2B,'print_screen': 0x2C,'ins': 0x2D,'del': 0x2E,'help': 0x2F,'0': 0x30,'1': 0x31,'2': 0x32,'3': 0x33,'4': 0x34,'5': 0x35,'6': 0x36,'7': 0x37,'8': 0x38,'9': 0x39,'a': 0x41,'b': 0x42,'c': 0x43,'d': 0x44,'e': 0x45,'f': 0x46,'g': 0x47,'h': 0x48,'i': 0x49,'j': 0x4A,'k': 0x4B,'l': 0x4C,'m': 0x4D,'n': 0x4E,'o': 0x4F,'p': 0x50,'q': 0x51,'r': 0x52,'s': 0x53,'t': 0x54,'u': 0x55,'v': 0x56,'w': 0x57,'x': 0x58,'y': 0x59,'z': 0x5A,'numpad_0': 0x60,'numpad_1': 0x61,'numpad_2': 0x62,'numpad_3': 0x63,'numpad_4': 0x64,'numpad_5': 0x65,'numpad_6': 0x66,'numpad_7': 0x67,'numpad_8': 0x68,'numpad_9': 0x69,'multiply_key': 0x6A,'add_key': 0x6B,'separator_key': 0x6C,'subtract_key': 0x6D,'decimal_key': 0x6E,'divide_key': 0x6F,'F1': 0x70,'F2': 0x71,'F3': 0x72,'F4': 0x73,'F5': 0x74,'F6': 0x75,'F7': 0x76,'F8': 0x77,'F9': 0x78,'F10': 0x79,'F11': 0x7A,'F12': 0x7B,'F13': 0x7C,'F14': 0x7D,'F15': 0x7E,'F16': 0x7F,'F17': 0x80,'F18': 0x81,'F19': 0x82,'F20': 0x83,'F21': 0x84,'F22': 0x85,'F23': 0x86,'F24': 0x87,'num_lock': 0x90,'scroll_lock': 0x91,'left_shift': 0xA0,'right_shift ': 0xA1,'left_control': 0xA2,'right_control': 0xA3,'left_menu': 0xA4,'right_menu': 0xA5,'browser_back': 0xA6,'browser_forward': 0xA7,'browser_refresh': 0xA8,'browser_stop': 0xA9,'browser_search': 0xAA,'browser_favorites': 0xAB,'browser_start_and_home': 0xAC,'volume_mute': 0xAD,'volume_Down': 0xAE,'volume_up': 0xAF,'next_track': 0xB0,'previous_track': 0xB1,'stop_media': 0xB2,'play/pause_media': 0xB3,'start_mail': 0xB4,'select_media': 0xB5,'start_application_1': 0xB6,'start_application_2': 0xB7,'attn_key': 0xF6,'crsel_key': 0xF7,'exsel_key': 0xF8,'play_key': 0xFA,'zoom_key': 0xFB,'clear_key': 0xFE,'+': 0xBB,',': 0xBC,'-': 0xBD,'.': 0xBE,'/': 0xBF,'`': 0xC0,';': 0xBA,'[': 0xDB,'\\': 0xDC,']': 0xDD,"'": 0xDE,'`': 0xC0
}
'''

系统键盘事件(组合键)的综合应用(输入框内容,全选、剪切、粘贴):

from selenium import webdriver
from selenium.webdriver import ActionChains
from selenium.webdriver.common.keys import Keys
import win32clipboard as w
import win32con
import time
import win32apiVK_CODE ={'enter':0x0D,'ctrl':0x11,'a':0x41,'v':0x56,'x':0x58}#键盘键按下
def keyDown(keyName):win32api.keybd_event(VK_CODE[keyName], 0, 0, 0)#键盘键抬起
def keyUp(keyName):win32api.keybd_event(VK_CODE[keyName], 0, win32con.KEYEVENTF_KEYUP, 0)driver = webdriver.Chrome()
url = "https://www.baidu.com"
driver.get(url)driver.find_element_by_id("kw").click()
driver.find_element_by_id("kw").send_keys("野生动物")
time.sleep(1)
keyDown('ctrl')
keyDown('a')
# 释放ctrl + a组合键
keyUp('a')
keyUp('ctrl')keyDown('ctrl')
keyDown('x')
# 释放ctrl + x组合键
keyUp('x')
keyUp('ctrl')
time.sleep(2)keyDown('ctrl')
keyDown('v')
# 释放ctrl + v组合键
keyUp('v')
keyUp('ctrl')driver.find_element_by_id("kw").send_keys("保护")driver.find_element_by_id('su').click()time.sleep(3)
driver.quit()

ActionChains 键盘组合事件:

from selenium import webdriver
from selenium.webdriver import ActionChains
from selenium.webdriver.common.keys import Keys
import timedriver = webdriver.Chrome()
url = "https://www.baidu.com"
driver.get(url)search_box = driver.find_element_by_id('kw')  # 定位到输入框
search_box.send_keys("hiphop")
driver.find_element_by_id("kw").click()  # 点击输入框,保证焦点在输入框ActionChains(driver).key_down(Keys.CONTROL).send_keys('a').key_up(Keys.CONTROL).perform()  # Ctrl + a 组合键
time.sleep(2)
ActionChains(driver).key_down(Keys.CONTROL).send_keys('x').key_up(Keys.CONTROL).perform()  # Ctrl + x 组合键
time.sleep(2)
ActionChains(driver).key_down(Keys.CONTROL).send_keys('v').key_up(Keys.CONTROL).perform()  # Ctrl + v 组合键
time.sleep(2)driver.find_element_by_id('su').click()  # 点击搜索按钮time.sleep(3)
driver.quit()

4)鼠标操作

from selenium import webdriver
from selenium.webdriver import ActionChains# 鼠标右键
ActionChains(driver).context_click().perform()# 鼠标左键与释放
ActionChains(driver).click_and_hold("div").perform()
ActionChains(driver).release("div").perform()# 鼠标悬浮
ActionChains(driver).move_to_element("元素").perform()# 拖拽
ActionChains(driver).drag_and_drop("被拖拽的元素", "目标位置").perform()
ActionChains(driver).drag_and_drop_by_offset("被拖拽的元素", 10, 10).perform()  # 向右下拖动10个像素

5)单选框操作

from selenium import webdriver
from selenium.webdriver.common.keys import Keys
import timedriver = webdriver.Chrome()
driver.get("http://39.100.104.214/test_radio.html")# 定位到“草莓”单选框
radio = driver.find_element_by_xpath("//input[@value='berry']")
if not radio.is_selected():radio.click()
time.sleep(2)#定位到“西瓜”单选框
radio = driver.find_element_by_xpath("//input[@value='watermelon']")
if not radio.is_selected():radio.click()
# 断言单选框是否被选中
assert radio.is_selected() == True# 遍历所有单选框,依次选择
for radio in driver.find_elements_by_xpath("//input[@name='fruit']"):time.sleep(1)radio.click()#遍历所有单选框
for radio in driver.find_elements_by_xpath("//input[@name='fruit']"):# 当单选框value为“berry”时,进行选择if radio.get_attribute("value") == "berry":time.sleep(1)radio.click()time.sleep(2)
driver.quit()

6)多选框操作

from selenium import webdriver
from selenium.webdriver.common.keys import Keys
import timedriver = webdriver.Chrome()
driver.get("http://39.100.104.214/test_checkbox.html")# 定位到“草莓”复选框
check_box = driver.find_element_by_xpath("//input[@value='berry']")
if not check_box.is_selected():# 选择“草莓”复选框(第一次选择)check_box.click()time.sleep(2)# 取消选择“草莓”复选框(第二次重复选择)check_box.click()time.sleep(2)
# 遍历所有复选框
for check_box in driver.find_elements_by_xpath("//input[@name='fruit']"):check_box.click()  # 依次选择复选框time.sleep(2)
driver.quit()

7)操作 JS 框

# 操作JS的Alert弹窗
alert = driver.switch_to.alert()
alert.accept()# 操作JS的confirm弹窗
alert = driver.switch_to.alert()
alert.accept()  # "确定"按钮
alert.dismiss()  # "取消"按钮# 操作JS的Prompt弹窗
alert = driver.switch_to.alert()
alert.send_keys("弹窗中输入自定义文本")
alert.accept()  # "确定"按钮
alert.dismiss()  # "取消"按钮

8)切换 frame

iframe:

# 使用索引值切换frame
driver.switch_to.frame(0)# 回到主frame页面(才能继续切换到其他frame)
driver.switch_to.default_content()
# 通过其他frame元素对象切换
driver.switch_to.frame(self.driver.find_element_by_tag_name("frame")[0])driver.switch_to.default_content()
driver.switch_to.frame(self.driver.find_element_by_id("rightframe"))

9)使用 JS 操作页面对象

from selenium import webdriver
import timedriver = webdriver.Chrome()
url = "http://www.sogou.com"
# 访问baidu首页
driver.get(url)# 当selenium的click不好用时
# JS执行输入和点击
searchInputBoxJS = "document.getElementById('query').value='hiphop';"
searchButtonJS = "document.getElementById('stb').click()"driver.execute_script(searchInputBoxJS)
driver.execute_script(searchButtonJS)time.sleep(3)
assert '嘻哈' in driver.page_sourcedriver.quit()

10)操作滚动条

from selenium import webdriverdriver = webdriver.Chrome()
url = "http://www.sohu.com"
driver.get(url)# 移动到末尾
driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
# 移动到中间
driver.execute_script("window.scrollTo(0, document.body.scrollHeight/2);")# 基于第多少个标签的元素来移动(若使用相同的个数则不会移动)
driver.execute_script("document.getElementsByTagName('a')[100].scrollIntoView(true);")
# 基于像素移动
driver.execute_script("window.scrollBy (0,400);")

11)浮动(联想)选项选择

# 方法一:模拟按键
searchBox = self.driver.find_element_by_id("").send_keys("嘻哈")
searchBox.send_keys(Keys.DOWN)
searchBox.send_keys(Keys.ENTER)# 方法二:匹配模糊内容
suggestion_option = self.driver.find_element_by_xpath("//ul/li[contains(., '篮球公园')]")
suggestion_option.click()# 方法三:通过索引(索引从1开始)
suggestion_option = self.driver.find_element_by_xpath("//ul/li[3]")

12)更改页面对象的属性值

注意:只针对当前会话有效,页面源码并没有真正改变。

使用场景:当原本的元素难以定位或操作时,可临时使用该方法为元素添加属性以方便后续继续操作。

from selenium import webdriver
import timedef addAttribute(driver, elementObj, attributeName, value):# 封装向页面标签中添加新属性方法# 调用JavaScript代码给页面标签添新属性,arguments [0] - [2] 分别会用后面的# element、attributeName和value参数值进行替换,并执行该JavaScript代码# 添加新属性的JavaScript代码语法为:element.attributeName = value# 比如input.name="test"driver.execute_script("arguments[0].%s=arguments[1]" %attributeName, elementObj, value)def setAttribute(driver, elementObj, attributeName, value):# 封装设置页面对象的属性值的方法# 调用JavaScript代码修改页面元素的属性值,arguments [0] - [2] 分别会用后面的# element、attributeName和value参数值进行替换,并执行该JavaScript代码driver.execute_script("arguments[0].setAttribute(arguments[1],arguments[2])", elementObj, attributeName, value)def getAttribute(elementObj, attributeName):# 封装获取页面对象的属性值的方法return elementObj.get_attribute(attributeName)def removeAttribute(driver, elementObj, attributeName):# 封装删除页面元素属性的方法# 调用JavaScript代码删除页面元素的指定的属性,arguments [0] - [1] 分别会用后面的# element、attributeName参数值进行替换,并执行该JavaScript代码driver.execute_script("arguments[0].removeAttribute(arguments[1])", elementObj, attributeName)driver = webdriver.Chrome()
url = "http://39.100.104.214/test_change_attr.html"
driver.get(url)element = driver.find_element_by_xpath("//input")# 给输入框对象新增属性name
addAttribute(driver, element, 'name', "search")
print ('添加的新属性值%s="%s"' %("name", getAttribute(element, "name")))time.sleep(3)
print ("更改文本框中的内容前的内容:", getAttribute(element, "value"))
# 更改input页面元素的value属性值为:这是更改后的文字内容:xxxxx
setAttribute(driver, element, "value", "xxxxxxx")time.sleep(3)
print ("更改文本框中内容后的内容:", getAttribute(element, "value"))# 修改输入框的长度
setAttribute(driver, element, "size", 20)
print ("更改后文本框标签中的size属性值:", getAttribute(element, "size"))time.sleep(3)
# 删除输入框里面的值
removeAttribute(driver, element, "value")
print ("删除value属性值后value属性值:", getAttribute(element, "value"))driver.quit()

13)文件下载 

示例 1:Chrome 版

from selenium import webdriver
import time
from selenium.webdriver.chrome.options import Optionsoptions = Options()
options.add_experimental_option("prefs", {"download.default_directory": "e:\\",  # 指定下载目录"download.prompt_for_download": False,"download.directory_upgrade": True,"safebrowsing.enabled": True})# 初始化配置
driver = webdriver.Chrome(chrome_options=options)url = "http://mirrors.hust.edu.cn/apache/zzz/mirror-tests/"
driver.get(url)# 点击下载文件
driver.find_element_by_partial_link_text("tar.gz").click()# 等待文件下载(等待时间根据实际情况而定)
time.sleep(10)
driver.quit()

示例 2:Firefox 版

from selenium import webdriver# 无人工干预地自动下载文件的相关配置
def setProfile():# 创建一个FirefoxProfile实例,用于存放自定义配置profile = webdriver.FirefoxProfile()# 指定下载路径,默认只会创建一级目录,如果指定了多级不存在的目录,将会# 下载到默认路径profile.set_preference("browser.download.dir", "d:\\iDownload")# browser.download.folderList:2表示下载到指定路径;0表示下载到桌面;# 1表示下载到默认路径profile.set_preference("browser.download.folderList", 2)# browser.helperApps.alwaysAsk.force:对于未知的MIME类型文件会弹出# 窗口让用户处理,默认值为True,设定为False表示不会记录打开未知MIME类型# 文件的方式。profile.set_preference("browser.helperApps.alwaysAsk.force", False)# 在开始下载是是否显示下载管理器profile.set_preference("browser.download.manager.showWhenStarting", False)# False隐藏下载框profile.set_preference("browser.download.manager.useWindow", False)# 默认值为True,False表示不获取焦点profile.set_preference("browser.download.manager.focusWhenStarting", False)# 下载EXE文件弹出警告;False表示不弹出警告框profile.set_preference("browser.download.manager.alertOnEXEOpen", False)# browser.helperApps.neverAsk.openFile表示直接打开下载文件,不显示确认框# 默认值为空字符串,文件类型可用逗号隔开,如"application/exe","application/excel"profile.set_preference("browser.helperApps.neverAsk.openFile", "application/pdf")# 对所给出文件类型不再弹出提示框进行询问,直接保存到本地磁盘profile.set_preference("browser.helperApps.neverAsk.saveToDisk","application/zip, application/octet - stream")# 下载完成后是否显示完成提示框profile.set_preference("browser.download.manager.showAlertOnComplete", False)# 下载结束后是否自动关闭下载框;False表示不关闭profile.set_preference("browser.download.manager.closeWhenDone", False)return profile# 启动浏览器时,添加设定好的自定义配置
driver = webdriver.Firefox(firefox_profile=setProfile())url = "https://www.python.org/downloads/release/python-2712/"
driver.get(url)
# 找到Python2.7.12下载页面中链接文字为“Windows x86-64 MSI installer”
# 的链接页面元素,点击进行无人工干预的下载Python2.7.12解释器文件
driver.find_element_by_link_text\("Windows x86-64 MSI installer").click()
# 等待文件下载完成,根据各自的网络带宽情况设定等待相应的时间
time.sleep(100)

14)文件上传

方式 1:使用 send_keys 上传文件.

from selenium import webdriver
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
import timedriver = webdriver.Chrome()
url= 'http://39.100.104.214/test_upload_file.html'
driver.get(url)wait = WebDriverWait(driver, 10, 0.2)
# 显示等待判断被测试页面上的上传文件按钮是否处于可被点击状态
wait.until(EC.element_to_be_clickable((By.ID, 'file')))
# 选择文件按钮直接输入文件名
driver.find_element_by_id("file").send_keys("e:\\file.txt")time.sleep(3)
# 提交文件
fileSubmitButton = driver.find_element_by_id("filesubmit")
fileSubmitButton.click()time.sleep(2)
driver.close()

方式 2:使用键盘事件

注意:键盘事件无法使用并发,仅适用单个用例执行的情况.

from selenium import webdriver
import unittest
import time
import traceback
import win32clipboard as w
import win32api
import win32con
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException, NoSuchElementException# 用于设置剪切板内容
def setText(aString):w.OpenClipboard()w.EmptyClipboard()w.SetClipboardData(win32con.CF_UNICODETEXT, aString)w.CloseClipboard()# 键盘按键映射字典
VK_CODE = {'enter':0x0D,'ctrl':0x11,'v':0x56}# 键盘键按下
def keyDown(keyName):win32api.keybd_event(VK_CODE[keyName], 0, 0, 0)
# 键盘键抬起
def keyUp(keyName):win32api.keybd_event(VK_CODE[keyName], 0, win32con.KEYEVENTF_KEYUP, 0)driver = webdriver.Chrome()
url= 'http://39.100.104.214/test_upload_file.html'
driver.get(url)
wait = WebDriverWait(driver, 10, 0.2)
# 显示等待判断被测试页面上的上传文件按钮是否处于可被点击状态
wait.until(EC.element_to_be_clickable((By.ID, 'file')))setText(u"c:\\test.txt")
driver.find_element_by_id("file").click()time.sleep(2)
# 模拟键盘按下ctrl + v组合键
keyDown("ctrl")
keyDown("v")
# 模拟键盘释放Ctrl + v组合键
keyUp("v")
keyUp("ctrl")
time.sleep(1)
# 模拟键盘按下回车键
keyDown("enter")
# 模拟键盘释放回车键
keyUp("enter")
# 暂停查看上传的文件time.sleep(3)
fileSubmitButton = driver.find_element_by_id("filesubmit")
fileSubmitButton.click()

15)操作日期控件

from selenium import webdriver
import timedriver = webdriver.Chrome()
url= 'http://jqueryui.com/resources/demos/datepicker/other-months.html'
driver.get(url)
# 操作日期控件
wait = WebDriverWait(driver, 10, 0.2)
# 显式等待日期控件对象
wait.until(EC.element_to_be_clickable((By.ID, "datepicker")))# 方式1:直接输入值
input_box.send_keys("11/24/2016")
# 注意:若遇到日期控件不允许用户输入,则可通过JS改变页面元素属性值来将日期控件修改成可编辑状态。time.sleep(3)
input_box.clear()
# 方式2:定位日期控件中的日期值对象
input_box.click()
date = driver.find_element_by_xpath("//a[.='19']")
date.click()

16)操作富文本框

示例:操作邮件正文的富文本框。

  • 方式 1:通过执行 JS

  • 方式 2:找到富文本框对象直接输入

from selenium import webdriver
import timedriver = webdriver.Firefox()
url = "http://mail.sohu.com"
driver.get(url)# 进行登录
time.sleep(3)
userName = driver.find_element_by_xpath('//input[@placeholder="请输入您的邮箱"]')
userName.clear()
userName.send_keys("xxx")
passWord = driver.find_element_by_xpath('//input[@placeholder="请输入您的密码"]')
passWord.clear()
passWord.send_keys("xxx")
login = driver.find_element_by_xpath(u'//input[@value="登 录"]')
login.click()# 进入“写邮件”页面
time.sleep(3)
driver.find_element_by_xpath(u'//li[text()="写邮件"]').click()
time.sleep(1)
# 收件人
receiver = driver.find_element_by_xpath('//div[@arr="mail.to_render"]//input')
time.sleep(1)
receiver.send_keys("fosterwu@sohu.com")
time.sleep(1)
# 标题
subject = driver.find_element_by_xpath('//input[@ng-model="mail.subject"]')
subject.send_keys("一封测试邮件!")
time.sleep(1)
iframe = driver.find_element_by_xpath('//iframe[contains(@id, "ueditor")]')# 切入到富文本区域的iframe中
driver.switch_to.frame(iframe)
time.sleep(1)
# 定位富文本框
editBox = driver.find_element_by_xpath("/html/body")# 方式1:通过执行JS方式输入富文本内容
driver.execute_script("document.getElementsByTagName('body')[0].innerHTML='邮件的正文内容;'")# 方式2:给富文本框元素直接输入内容
editBox.send_keys("邮件的正文内容")# 从iframe里面切换出来
driver.switch_to.default_content()
time.sleep(1)
# 点击“发送”
driver.find_element_by_xpath('//span[.="发送"]').click()time.sleep(1)
driver.quit()
  • 方式 3:使用键盘事件
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
import win32clipboard as w
import win32api, win32con
import time# 用于设置剪切板内容
def setText(aString):w.OpenClipboard()w.EmptyClipboard()w.SetClipboardData(win32con.CF_UNICODETEXT, aString)w.CloseClipboard()# 键盘按键映射字典
VK_CODE = {'ctrl':0x11, 'v':0x56}# 键盘键按下
def keyDown(keyName):win32api.keybd_event(VK_CODE[keyName], 0, 0, 0)# 键盘键抬起
def keyUp(keyName):win32api.keybd_event(VK_CODE[keyName], 0, win32con.KEYEVENTF_KEYUP, 0)driver = webdriver.Firefox()
url = "http://mail.sohu.com"
driver.get(url)time.sleep(2)
userName = driver.find_element_by_xpath('//input[@placeholder="请输入您的邮箱"]')
userName.clear()
userName.send_keys("xxx")
passWord = driver.find_element_by_xpath('//input[@placeholder="请输入您的密码"]')
passWord.clear()
passWord.send_keys("xxx")
login = driver.find_element_by_xpath(u'//input[@value="登 录"]')
login.click()time.sleep(2)
driver.find_element_by_xpath(u'//li[text()="写邮件"]').click()time.sleep(1)
receiver = driver.find_element_by_xpath('//div[@arr="mail.to_render"]//input')receiver.send_keys("fosterwu@sohu.com")time.sleep(1)
subject = driver.find_element_by_xpath('//input[@ng-model="mail.subject"]')subject.send_keys("一封测试邮件!")# 将焦点移动到正文
subject.send_keys(Keys.TAB)
subject.send_keys(Keys.TAB)setText(u"邮件正文内容")
# 将剪贴板的内容粘贴到当前焦点即邮件正文中
keyDown("ctrl")
keyDown("v")
keyUp("v")
keyUp("ctrl")time.sleep(1)
driver.find_element_by_xpath('//span[.="发送"]').click()driver.quit()

17)高亮显示操作元素

from selenium import webdriver
import timedef highLightElement(driver,element):# 封装好的高亮显示页面元素的方法# 使用JavaScript代码将传入的页面元素对象的背景颜色和边框颜色分别设置为绿色和红色driver.execute_script("arguments[0].setAttribute('style',\arguments[1]);", element,"background:green; border:2px solid red;")driver = webdriver.Chrome()url = "http://sogou.com"
driver.get(url)
searchBox = driver.find_element_by_id("query")highLightElement(driver, searchBox)
time.sleep(3)
searchBox.send_keys("hiphop")submitButton = driver.find_element_by_id("stb")
highLightElement(driver, submitButton)
time.sleep(3)
submitButton.click()time.sleep(3)
driver.quit()

18)切换浏览器标签页

方式 1:遍历标签页对象

from selenium import webdriver
import time
import win32api, win32conVK_CODE ={'ctrl':0x11, 't':0x54, 'tab':0x09}# 键盘键按下
def keyDown(keyName):win32api.keybd_event(VK_CODE[keyName], 0, 0, 0)
# 键盘键抬起
def keyUp(keyName):win32api.keybd_event(VK_CODE[keyName], 0, win32con.KEYEVENTF_KEYUP, 0)# 封装的按键方法
def simulateKey(firstKey, secondKey):keyDown(firstKey)keyDown(secondKey)keyUp(secondKey)keyUp(firstKey)# 使用快捷键 ctrl+t 新增两个标签页
driver = webdriver.Chrome()
time.sleep(1)
for i in range(2):simulateKey("ctrl", "t")# 将焦点切回第一个标签页
simulateKey("ctrl", "tab")
driver.get("http://sogou.com")
driver.find_element_by_id("query").send_keys("hiphop")
driver.find_element_by_id("stb").click()
time.sleep(3)flag = Trueall_handles = driver.window_handles
for handle in all_handles:driver.switch_to.window(handle)if "知乎" not in driver.page_source and flag:driver.get("http://www.baidu.com")driver.find_element_by_id("kw").send_keys("hiphop")driver.find_element_by_id("su").click()flag = Falsetime.sleep(2)assert "嘻哈" in driver.page_sourceelif ("知乎" not in driver.page_source) and ("hiphop" not in driver.page_source):driver.get("http://www.iciba.com")assert "查词" in driver.page_sourcedriver.quit()

方式 2:使用标签页对象索引

from selenium import webdriver
import time
import win32api, win32conVK_CODE ={'ctrl':0x11, 't':0x54, 'tab':0x09}# 键盘键按下
def keyDown(keyName):win32api.keybd_event(VK_CODE[keyName], 0, 0, 0)
# 键盘键抬起
def keyUp(keyName):win32api.keybd_event(VK_CODE[keyName], 0, win32con.KEYEVENTF_KEYUP, 0)# 封装的按键方法
def simulateKey(firstKey, secondKey):keyDown(firstKey)keyDown(secondKey)keyUp(secondKey)keyUp(firstKey)driver = webdriver.Chrome()
time.sleep(1)# 新增两个标签页
for i in range(2):simulateKey("ctrl", "t")
time.sleep(1)# 把焦点切换回第一个标签页
simulateKey("ctrl", "tab")
driver.get("http://sogou.com")
driver.find_element_by_id("query").send_keys("hiphop")
driver.find_element_by_id("stb").click()
time.sleep(2)all_handles = driver.window_handles
# 切换到第二个窗口句柄
driver.switch_to.window(all_handles[1])
driver.get("http://www.baidu.com")
driver.find_element_by_id("kw").send_keys("hiphop")
driver.find_element_by_id("su").click()
time.sleep(2)#切换到第三个窗口句柄
driver.switch_to.window(all_handles[2])
driver.get("http://www.baidu.com")
driver.find_element_by_id("kw").send_keys("街舞")
driver.find_element_by_id("su").click()
time.sleep(2)driver.quit()

19)操作表格

# 操作表格的工具类
class Table:# 定义一个私有属性__table,用于存放table对象__table = ""def __init__(self, table):self.setTable(table)def setTable(self, table):self.__table = tabledef getTable(self):return self.__tabledef getRowCount(self):# 返回行数return len(self.__table.find_element_by_tag_name("tr"))def getColumnCount(self):# 返回列数return len(self.__table.find_element_by_tag_name("td"))def getCell(self, rowNo, colNo):# 获取表格中某行某列的单元格对象try:currentRow = self.__table.find_element_by_tag_name("tr")[rowNo - 1]currentCell = currentRow.find_element_by_tag_name("td")[colNo - 1]return currentCellexcept Exception as e:raise edef getElementInCell(self, rowNo, colNo, by, value):# 获取表格中某行某列的单元格对象try:currentRow = self.__table.find_element_by_tag_name("tr")[rowNo - 1]currentCell = currentRow.find_element_by_tag_name("td")[colNo - 1]element = currentCell.find_element(by=by, value=value)return elementexcept Exception as e:raise e# 测试示例
webTable = driver.find_element_by_tag_name("table")
table = Table(webTable)
# 获取表格中第二行第三列单元格对象
cell = table.getCell(2, 3)
self.assertAlmostEqual("第二行第三列", cell.text)
cellInput = table.getElementInCell(3, 2, "tag name", "input")
cellInput.send_keys("……")

5、测试 HTML5 的视频播放器

from selenium import webdriver
import timedriver = webdriver.Chrome()
url = "https://www.w3school.com.cn/tiy/t.asp?f=html5_av_met_addtexttrack"
driver.get(url)# 切换到视频播放器所在的frame
driver.switch_to.frame(driver.find_element_by_xpath("//iframe[@id='iframeResult']"))# 定位视频播放器对象
videoPlayer = driver.find_element_by_tag_name("video")# 通过播放器内部的currentSrc属性获取视频文件的网络存储地址
videoSrc = driver.execute_script("return arguments[0].currentSrc;", videoPlayer)
print(videoSrc)
assert videoSrc == "https://www.w3school.com.cn/example/html5/mov_bbb.mp4"# 通过播放器内部的currentSrc属性获取视频文件的播放时长
videoDuration = driver.execute_script("return arguments[0].duration;", videoPlayer)
print (videoDuration)#点击视频元素,让视频开始播放
videoPlayer.click()
time.sleep(2)#点击视频元素,让视频停止播放,并截屏
videoPlayer.click()
driver.save_screenshot("e:\\videoPlay_pause.png")#点击视频元素,让视频再次播放
videoPlayer.click()
time.sleep(2)driver.quit()

6、断言

如果断言失败,程序会抛异常,(未捕捉异常的话)程序就不再继续往下执行。

示例:断言网页源码中是否出现某关键字。

from selenium import webdriver
from selenium.webdriver.common.keys import Keys
import timedriver = webdriver.Chrome()
driver.get("http://39.100.104.214/test_checkbox.html")try:# 断言草莓是否存在于网页源码中assert "草莓" in driver.page_source
except AssertionError:print("断言失败!")time.sleep(2)
driver.quit()

7、截屏

1. 浏览器截屏

  • 只能截当前浏览器屏幕,不能截滚动屏
  • 一般情况下,是断言失败,或者错误时才会使用截屏
  • 提示:IE浏览器截屏图片会出现大黑边
from selenium import webdriver
import timedriver = webdriver.Chrome()
driver.get("http://www.baidu.com")# 截屏,并保存到本地
driver.get_screenshot_as_file("e:\\test.png")time.sleep(2)
driver.quit()

2. 操作系统截屏

  • 需要先安装 pillow 包:pip install pillow
  • 只能截当前屏幕,不能截滚动屏
from selenium import webdriver
from PIL import ImageGrab  # 需要先安装 pillow 包
import timedriver = webdriver.Chrome()
driver.get("http://www.baidu.com")# 最大化浏览器窗口
driver.maximize_window()
time.sleep(1)
im = ImageGrab.grab()  # 操作系统截屏
im.save("e:\\test2.jpg")time.sleep(2)
driver.quit()

8、隐式等待

  • 只需要设置一次,将在整个driver生命周期起作用。
  • 弊端:等页面全部元素加载后才能往下执行代码,容易被某个JS所影响效率。
from selenium import webdriver
import timedriver = webdriver.Chrome()url = "http://39.100.106.61:8080/selenium/test_wait.html"
driver.get(url)# 给后续所有页面元素定位加一个时间上限(10秒)
# 若10秒后该元素仍定位不到,则抛异常
# 若定位到则马上执行后续代码
driver.implicitly_wait(10)#手工点击页面按钮,则可以显示h1标签
driver.find_element_by_xpath("//h1")driver.quit()

9、显式等待

原理:每隔一段时间(默认 0.5 秒)执行一下自定义的判定条件,直到超过设定的最大时长,然后抛出 TimeoutException。

  • WebDriverWait(self, WebDriver实例对象, 最长等待时间, 调用频率, 执行过程中忽略的异常类型,默认只忽略NoSuchElementException)
  • WebDriverWait.until(method, message("抛出的异常提示信息"))  # 在规定等待时间内,每隔调用频率调用一次 method,直到其返回值不为 False。
  • WebDriverWait.until_not(method, message("抛出的异常提示信息"))  # 直到其返回值不为True。
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC# 显式等待对象(最多等10秒,每0.2秒判断一次等待的条件):等待某元素出现,然后输入本文。
WebDriverWait(driver, 10, 0.2).until(lambda x: x.find_element_by_id("")).send_keys("某某被找到")# 显式等待中的期望场景
wait = WebDriverWait(driver, 10)
# alert_is_present():判断页面是否出现alert框,在10秒内直到出现alert框才会往下执行代码
wait.until(EC.alert_is_present()).text

示例:常见显式等待条件

from selenium import webdriver
from selenium.webdriver import ActionChains
# 导入By类
from selenium.webdriver.common.by import By
# 导入显示等待类
from selenium.webdriver.support.ui import WebDriverWait
# 导入期望场景类
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException, NoSuchElementException
import time
import tracebackdriver = webdriver.Chrome()
url = "http://39.100.104.214/test_explicity_wait.html"# 访问测试网页
driver.get(url)
try:wait = WebDriverWait(driver, 10, 0.2)wait.until(EC.title_is("你喜欢的水果"))print("网页标题是'你喜欢的水果'")# 等待10秒,直到要找的按钮出现element = WebDriverWait(driver, 10).until(lambda x: x.find_element_by_xpath("//input[@value='Display alert box']"))element.click()# 等待alert框出现alert = wait.until(EC.alert_is_present())# 打印alert框体消息print(alert.text)# 确认警告信息alert.accept()# 获取id属性值为"peach"的页面元素peach = driver.find_element_by_id("peach")# 判断id属性值为"peach"的页面元素是否能被选中peachElement = wait.until(EC.element_to_be_selected(peach))print("下拉列表的选项'桃子'目前处于选中状态")# 判断复选框是否可见并且能被点击wait.until(EC.element_to_be_clickable((By.ID, 'check')))print("复选框可见并且能被点击")
except TimeoutException as e:# 捕获TimeoutException异常print(traceback.print_exc())
except NoSuchElementException as e:# 捕获NoSuchElementException异常print(traceback.print_exc())
except Exception as e:# 捕获其他异常print(traceback.print_exc())
driver.quit()

10、操作cookies

# 操作浏览器的cookie
cookies = driver.get_cookies()
for cookie in cookies:print("%s -> %s -> %s -> %s -> %s" % cookie["domain"], cookie["name"], cookie["value"],cookie["expiry"], cookie["path"])# 根据Cookie的name值获取该条cookie信息
cookie = driver.get_cookie("AB")
# 删除
print(driver.delete_cookie("AB"))
# 删除全部cookie
driver.delete_all_cookies()# 添加自定义Cookie信息
driver.add_cookie({"name":"hippop", "value":"in China"})
print(driver.get_cookie("hippop"))

11、页面加载超时

from selenium.common.exceptions import TimeoutException
import traceback# 指定页面加载超时时间,当到达等待时间(4秒)则不再继续等待,而是继续执行后续操作
driver.set_page_load_timeout(4)try:driver.get("http://www.baidu.com")
except TimeoutException:# print(traceback.print_exc())print("页面加载超过设定时间")# 通过执行JS来停止加载,然后继续执行后续动作driver.execute_script("window.stop()")driver.switch_to.alert()

12、结束 Windows 的浏览器进程

from selenium import webdriver
import os
import time# 分别启动3个浏览器
driver1 = webdriver.Chrome()
url = "http://www.sogou.com"
# 访问baidu首页
driver1.get(url)driver2 = webdriver.Firefox()
url = "http://www.sogou.com"
# 访问baidu首页
driver2.get(url)driver3 = webdriver.Ie()
url = "http://www.sogou.com"
# 访问baidu首页
driver3.get(url)# 结束Chrome浏览器进程
returnCode = os.system("taskkill /F /iM chrome.exe")
if returnCode == 0:print ("结束Chrome浏览器进程成功!")
else:print ("结束Chrome浏览器进程失败!")# 结束Firefox浏览器进程
returnCode = os.system("taskkill /F /iM firefox.exe")
if returnCode == 0:print ("结束Firefox浏览器进程成功!")
else:print ("结束Firefox浏览器进程失败!")returnCode = os.system("taskkill /F /iM iexplore.exe")
if returnCode == 0:print ("结束IE浏览器进程成功!")
else:print ("结束IE浏览器进程失败!")

13、浏览器相关配置

针对不同的浏览器,需要下载不同的驱动程序(Driver)。

1. Firefox 相关配置

以 Firefox 浏览器为例,在https://github.com/mozilla/geckodriver/releases/网站中选择“geckodriver-v0.23.0-win64.zip”下载并解压文件后,把 Geckodriver.exe文件放在Python安装程序的目录下,也就是C:\Python36-32。切记Firefox浏览器的版本号一定要大于48,否则运行代码后会出现驱动程序(Driver)版本与Firefox浏览器不兼容的情况。

针对Firefox浏览器的测试代码如下: 

from selenium import webdriverprofile = webdriver.FirefoxProfile()
# 禁用CSS
profile.set_preference("permissions.default.stylesheet", 2)
# 禁用images加载
profile.set_preference("permissions.default.image", 2)
# 禁用Flash加载
profile.set_preference("dom.ipc.plugins.enabled.libflashplayer.so", False)

2. Firefox 启动带有配置信息 

若不设置进行下述配置,那么 webdriver 每次启动火狐浏览器,默认都是一个不太有任何插件的浏览器被启动。

通过配置的方式,指定一个浏览器设置来启动,就可以使用以前安装的插件或配置信息了。

步骤一:打开火狐配置窗口

cmd 执行下述命令,会弹出火狐配置窗口:

firefox.exe -ProfileManager -no-remote

步骤二:获取自定义配置文件的路径

步骤三:初始化配置路径 


from selenium import webdriver
import timeproPath = "C:\\Users\\wuxiaohua\\AppData\\Roaming\\Mozilla\\Firefox\\Profiles\\36ma49s2.WebDriver"profile = webdriver.firefox.firefox_profile.FirefoxProfile(proPath)# 将首页设置为搜狗
profile.set_preference("browser.startup.homepage", "http://www.sogou.com")
profile.set_preference("browser.startup.page", 1)# 初始化带配置信息的浏览器
driver = webdriver.Firefox(executable_path="e:\\geckodriver", firefox_profile=profile)driver.get("https://www.sohu.com")

3. Chrome 相关配置

from selenium import webdriver
import time
from selenium.webdriver.chrome.options import Options# 创建Chrome的一个Options实例对象
chrome_options = Options()# 禁用PDF和Flash插件
# 禁用Image加载
profile = {"plugins.plugins_disabled": ['Chrome PDF Viewer'],"plugins.plugins_disabled": ['Adobe Flash Player'],"profile.managed_default_content_settings.images": 2}# 自动将文件下载到指定路径。如果目录不存在,将会自动创建
profile["download.default_directory"] = "e:\\Download"# 加载profile
chrome_options.add_experimental_option("prefs", profile)# 添加禁用扩展插件的设置参数项
chrome_options.add_argument("--disable-extensions")
# 添加屏蔽提示信息
chrome_options.add_experimental_option("excludeSwitches", ["ignore-certificate-errors"])
# 浏览器最大化
chrome_options.add_argument("--start-maximized")# 测试爱奇艺首页的Flash和图片是否被禁止加载
if __name__ == "__main__":driver = webdriver.Chrome(chrome_options=chrome_options)url = "http://www.iqiyi.com"driver.get(url)time.sleep(10)driver.quit()

14、添加日志

1. 配置文件

定义日志的类型、级别、格式等信息。

Logger.conf:

[loggers]
# 定义了三种写日志的方式:logger_root、logger_example01、logger_example02
keys=root,example01,example02   
[logger_root]
# 日志的颗粒度级别:critical > error > warning > info > debug
level=DEBUG  
# 日志设置:设定了日志的格式、位置、级别等信息
handlers=hand01,hand02
[logger_example01]
handlers=hand01,hand02
# 日志格式名字
qualname=example01 
propagate=0
[logger_example02]
handlers=hand01,hand03
qualname=example02
propagate=0
###############################################
[handlers]
# 日志的设置:3种
keys=hand01,hand02,hand03
[handler_hand01]
# 流模式:打印到屏幕上(标准输出,标准错误输出)
class=StreamHandler  
# 日志级别
level=DEBUG  
# 日志的格式
formatter=form01 
# 标准错误输出
args=(sys.stderr,)
[handler_hand02]
# 日志打印到文件
class=FileHandler
level=DEBUG
formatter=form01
# 设定日志文件位置,a表示追加打印
args=('e:\\AutoTestLog.log', 'a') 
[handler_hand03]
# 回滚日志打印
class=handlers.RotatingFileHandler
level=INFO
formatter=form01
args=('e:\\AutoTestLog.log', 'a', 10*1024*1024, 5)
# 10*1024*1024:一个日志文件最大是10m,最多打印5个文件
# 打印超过50mb的日志信息,那么会把第一个日志文件做覆盖写
###############################################
[formatters]
keys=form01,form02
# form01格式,设定每一行日志的格式信息
[formatter_form01]
format=%(asctime)s %(filename)s[line:%(lineno)d] 【%(levelname)s】 %(message)s
# %(asctime):时间
# %(filename):当前执行的文件名
# %(lineno):当前执行代码行号
# %(levelname):日志的级别
# %(message):具体的日志信息
# 日期格式:年-月-日 时-分-秒
datefmt=%Y-%m-%d %H:%M:%S
[formatter_form02]
format=%(name)-12s: %(levelname)-8s %(message)s
datefmt=%Y-%m-%d %H:%M:%S

常用配置说明:

format: 指定输出的格式和内容,format 可以输出很多有用信息,如下所示:

  • %(levelno)s: 打印日志级别的数值
  • %(levelname)s: 打印日志级别名称
  • %(pathname)s: 打印当前执行程序的路径,其实就是sys.argv[0]
  • %(filename)s: 打印当前执行程序名
  • %(funcName)s: 打印日志的当前函数
  • %(lineno)d: 打印日志的当前行号
  • %(asctime)s: 打印日志的时间
  • %(thread)d: 打印线程ID
  • %(threadName)s: 打印线程名称
  • %(process)d: 打印进程ID
  • %(message)s: 打印日志信息

datefmt: 指定时间格式,同time.strftime()
level: 设置日志级别,默认为logging.WARNING
stream: 指定将日志的输出流,可以指定输出到sys.stderr,sys.stdout或者文件,默认输出到sys.stderr,当stream和filename同时指定时,stream被忽略

2. 工具类

封装 4 种日志级别的函数。

LogUtil.py:

import logging.config
import logging# 写绝对路径和相对路径都可以
logging.config.fileConfig("Logger.conf")
logger = logging.getLogger("example01")def debug(message):# 打印debug级别的日志方法logger.debug(message)def warning(message):# 打印warning级别的日志方法logger.warning(message)def info(message):# 打印info级别的日志方法logger.info(message)def error(message):# 打印error级别的日志方法logger.error(message)if __name__=="__main__":debug("hi")info("gloryroad")warning("hello")error("这是一个error日志")

3. 测试类

使用工具类进行测试。

testDemo.py:

from selenium import webdriver
from LogUtil import *driver = webdriver.Chrome()
debug("============== 开始搜索测试 ==============")
url = "http://www.sogou.com"
debug("访问网址:http://www.sogou.com")
driver.get(url)
debug("定位搜索框")
driver.find_element_by_id("query").send_keys("hiphop")
info("在输入框中输入搜索关键字串“hiphop”" + driver.find_element_by_id("query").get_attribute("value"))
debug("单击搜索按钮")
driver.find_element_by_id("stb").click()
debug("关闭浏览器")
driver.quit()
debug("============== 搜索测试结束 ==============")

屏幕及日志文件的打印内容:

2021-01-10 23:47:25 LogUtil.py[line:7] 【DEBUG】 ============== 开始搜索测试 ==============
2021-01-10 23:47:25 LogUtil.py[line:7] 【DEBUG】 访问网址:http://www.sogou.com
2021-01-10 23:47:27 LogUtil.py[line:7] 【DEBUG】 定位搜索框
2021-01-10 23:47:27 LogUtil.py[line:13] 【INFO】 在输入框中输入搜索关键字串“hiphop”hiphop
2021-01-10 23:47:27 LogUtil.py[line:7] 【DEBUG】 单击搜索按钮
2021-01-10 23:47:28 LogUtil.py[line:7] 【DEBUG】 关闭浏览器
2021-01-10 23:47:31 LogUtil.py[line:7] 【DEBUG】 ============== 搜索测试结束 ==============

15、测试工具类封装

生成以时间命名的目录,以存放异常截图或日志文件。

1. 文件工具类

模块主要用于获取当前的日期以及时间,用于生成保存截图文件目录名等场景。

FileUtil.py:

import time, os
from datetime import datetime# 输出当前时间格式:年-月-日
def currentDate():date = time.localtime()# 输出:time.struct_time(tm_year=2018, tm_mon=1, tm_mday=21, tm_hour=23, tm_min=27, tm_sec=43, tm_wday=6, tm_yday=21, tm_isdst=0)# 构造今天的日期字符串today = str(date.tm_year) + "-" + str(date.tm_mon) + "-" + str(date.tm_mday)return today# 输出当前时间格式:时-分-秒
def currentTime():timeStr = datetime.now()now = timeStr.strftime("%H-%M-%S")return now# 创建目录:年月日为父目录,时分秒为子目录
def createDir():# 获得当前文件所在目录的绝对路径currentPath = os.path.dirname(os.path.abspath(__file__))today = currentDate()dateDir = os.path.join(currentPath, today)print("日期目录:%s" % dateDir)if not os.path.exists(dateDir):# 如果以今天日期命名的目录不存在则创建os.mkdir(dateDir)now = currentTime()timeDir = os.path.join(dateDir, now)print("时间目录:%s" % timeDir)if not os.path.exists(timeDir):# 如果以今天日期命名的目录不存在则创建os.mkdir(timeDir)return timeDirif __name__ == "__main__":print(createDir())

2. 截屏工具类

封装异常截图。

ScreenShot.py:

from selenium import webdriver
import DateUtil
import os
import traceback
import time# 封装截屏方法
def take_screen_shot(driver, savePath, picName):# 构造截屏路径及图片名picPath = os.path.join(savePath, picName+".png")try:driver.get_screenshot_as_file(picPath)print("截图成功:%s" % picName+".png")except Exception:print("截图失败:%s" % traceback.print_exc())# 测试示例
if __name__ == "__main__":picDir = DateUtil.createDir()def test():try:# 序号用来作为文件名结尾,防止文件名重复num = 0driver = webdriver.Chrome()driver.get("http://www.baidu.com")assert "hiphop" in driver.page_sourceexcept AssertionError as e:num += 1take_screen_shot(driver, picDir, "AssertionError"+str(num))except Exception as e:num += 1take_screen_shot(driver, picDir, "Exception"+str(num))test()

16、Selenium元素定位实战

在UI自动化测试中,最基础最核心的技能是对页面元素进行定位,定位到相应的元素后才可以对页面的操作进行编码验证。

1. 调试工具实战

在 Chrome浏览器中,点击鼠标右键,在弹出的快捷菜单中选择“检查”选项;在弹出的调试信息窗口中,点击按钮后,将鼠标移动到需要定位的目标位置,调试信息窗口中就会显示元素的属性。
以对百度搜索页面的测试为例,在调试信息窗口中点击按钮后,将鼠标移动到百度搜索输入框上,屏幕上就会显示元素属性。

如图所示:

在图中可以看到,百度搜索输入框的元素属性 ID 为 kw,NAME 为wd,CLASS_NAME为s_ipt。

在浏览器中点击菜单栏的“工具”选项卡,在弹出的“工具”选项中点击“Web开发者”选项中的“Web控制台”子选项,如图所示。 

这里依然以对百度搜索页面的测试为例。点击Web控制台的按钮后,将鼠标移动到搜索输入框上,屏幕上显示百度搜索输入框的元素属性,如图所示。 

2. 单个元素定位实战

在Selenium自动化测试中,提供了单个元素定位方式和多个元素定位方式。两种方式都是根据元素属性 ID、NAME、CLASS_NAME、TAG_NAME、CSS_SELECTOR、XPATH、LINK_TEXT、PARTIAL_LINK_TEXT 来进行定位。下面就通过具体的实例说明单个元素定位在UI自动化测试中的应用。

1)find_element_by_id

通过元素属性 ID定位到元素,方法是 find_element_by_id。这里以百度搜索输入框为例,它的代码是:

它的ID属性是 kw,在百度搜索输入框中输入搜索的关键字“selenium”的代码如下: 

#!/usr/bin/env python 
#-*-coding:utf-8-*-from selenium import webdriverdriver=webdriver.Firefox()
driver.maximize_window()
driver.implicitly_wait(30)
driver.get('http://www.baidu.com')
driver.find_element_by_id('kw').send_keys('selenium')
driver.quit()

2)find_element_by_name

通过元素属性 NAME 定位到元素,方法是 find_element_by_name。它的NAME 元素属性是 wd,在百度搜索输入框中输入中实现搜索关键字的代码如下: 

#!/usr/bin/env python 
#-*-coding:utf-8-*-
from selenium import webdriverdriver=webdriver.Firefox()
driver.maximize_window()
driver.implicitly_wait(30)
driver.get('http://www.baidu.com')
driver.find element by name('wd').send_keys('selenium')
driver.quit()

3)find_element_by_class_name

通过元素属性 CLASS_NAME 定位到元素,方法是 find_element_by_class_name。还是以在百度搜索输入框中输入搜索关键字“selenium”为例,实现的代码如下:

#!/usr/bin/env python 
#-*-coding:utf-8-*-
from selenium import webdriver
driver=webdriver.Firefox()
driver.maximize_window()
driver.implicitly_wait(30)
driver.get('http://www.baidu.com')
driver.find_element_by_class_name('s_ipt').send_keys('selenium')
driver.quit()

4)find_element_by_xpath

通过XPATH定位百度搜索输入框的元素,方法是find_element_by_xpath,原始属性是//*[@id=”kw”]。获取的方式是定位到百度搜索输入框的元素属性后,用鼠标右键点击该属性,在弹出的快捷菜单中上选择“Copy”选项,在“Copy”子选项中选择“Copy Xpath”选项,如图所示。

以在百度搜索输入框中输入搜索关键字“selenium”为例,实现的代码如下:

#!/usr/bin/env python 
#-*-coding:utf-8-*-
from selenium import webdriver
driver=webdriver.Firefox()
driver.maximize_window()
driver.implicitly wait(30)
driver.get('http://www.baidu.com')
driver.find_element_by_xpath('/*[@id="kw"]').send_keys('selenium')
driver.quit()

5)find_element_by_link_text

LINK_TEXT用于对超链接的处理。在HTML的代码中主要是以标签a对应,方法是 find_element_by_link_text。以点击百度首页的“新闻”链接为例,查看“新闻”对应的代码:新闻。依据代码可以看到它是以 a 标签的。

下面实现点击百度首页的“新闻”链接,实现的代码如下:

#!/usr/bin/env python 
#-*-coding:utf-8-*-
from selenium import webdriver
driver=webdriver.Firefox()
driver.maximize window()
driver.implicitly_wait(30)
driver.get('http://www.baidu.com')
driver.find_element_by_link_text(u'新闻').click()
driver.quit()

6)find_element_by_partial_link_text

PARTIAL_LINK_TEXT 也用于对超链接的处理,它与 LINK_TEXT 不同的是,它是按模糊搜索方式处理的。例如,在百度首页包含“闻”字的只有新闻链接,那么操作的时候只需要填写“闻”字就可以定位成功,方法是 find_element_by_partial_link_text。

仍以实现点击百度首页的“新闻”链接为例,代码如下: 

#!/usr/bin/env python 
#-*-coding:utf-8-*-from selenium import webdriver
driver=webdriver.Firefox()
driver.maximize window()
driver.implicitly_wait(30)
driver.get('http://www.baidu.com')
driver.find_element_by_partial_link_text(u'闻').click()
driver.quit()

7)find_element_by_css_selector

当使用ID、NAME等方式定位不到元素的时候,可使用CSS_SELECTOR,方法是 find_element_by_css_selector。这里以百度搜索输入框为例,获取的方式是定位到百度搜索输入框的元素属性后,用鼠标右键点击该属性,在弹出的快捷菜单中上选择“Copy”选项,在“Copy”子选项中选择“Copy selector”选项,就可以获取到基于CSS_SELECTOR的元素属性是#kw,如图所示

在百度搜索输入框中输入搜索关键字“selenium”的代码如下: 

#!/usr/bin/env python 
#-*-coding:utf-8-*-
from selenium import webdriver
driver=webdriver.Firefox()
driver.maximize_window()
driver.implicitly_wait(30)
driver.get('http://www.baidu.com')
driver.find_element_by_css_selector('#kw').send_keys('Selenium')
driver.quit()

3. 多个元素定位实战

在工作中,某些时候可能会发现元素的ID、NAME、CLASS_NAME等元素属性是一致的,这时,使用ID、NAME、CLASS_NAME等这些元素属性定位时就无法准确地定位到具体的元素。例如,在百度首页中,以TAG_NAME元素属性来定位百度搜索输入框,在代码中发现不仅仅在百度搜索输入框有 input 标签,在百度搜索输入框之前也有 input 标签,这时该如何定位呢?这时可以使用多个元素定位的方式。当定位到多个元素后,结果将以列表形式呈现,然后可以按照列表的索引来定位到具体的元素位置。下面以ID、TAG_NAME为例进行讲解。

1)find_elements_by_tag_name

以百度搜索输入框为例,使用 TAG_NAME的方式来实现定位,它的TAG_NAME是input,首先来获取百度首页的input标签,并且查看它的类型,代码如下:

#!/usr/bin/env python #-*-coding:utf-8-*-from selenium import webdriverdriver=webdriver.Firefox()
driver.maximize_window()
driver.implicitlywait(30)driver.get('http://www.baidu.com')
tag_names=driver.find_elements_by_tag_name('input')
for tag_name in tag_names:print tag_name 
print type(tag_names)
driver.quit()

注解:从以上代码和输出结果中,可以看到,input的数据类型是 list,那么百度首页搜索输入框是在input标签中的第8位,也就是对应的索引是7(索引是从0开始)。

下面使用多个元素定位的方式,实现在百度搜索输入框中输入搜索关键字“selenium”。

实现的代码如下:

#!/usr/bin/env python 
#-*-coding:utf-8-*-from selenium import webdriverdriver=webdriver.Firefox()
driver.maximize_window()
driver.implicitly_wait(30)
driver.get('http://www.baidu.com')
driver.find_elements_by_tag_name('input')[7].send_keys('Selenium')
driver.quit()

2)find_elements_by_id

多个元素的定位思路都是一样的。下面通过一个具体的登录实例来看在ID一致的情况下,使用多个元素定位的解决方案。

HTML的代码如下:

在以上代码中可以看到用户名输入框、密码输入框,以及“登录”按钮的ID 一致,现在希望通过脚本输入用户名和密码,点击“登录”按钮,跳转到success.html的页面,并且使用 ID来定位。在以上代码中看到用户名和密码输入框,以及登录按钮的ID一致,实现的代码如下: 

#!/usr/bin/env python 
#-*-coding:utf-8-*-from selenium import webdriverdriver=webdriver.Firefox()
driver.maximize window()driver.implicitly_wait(30)
#index.html的地址依据自己本地实际地址
driver.get('file:///D:/git/github/book/testCase/index.html')
ids=driver.find_elements_by_id('login')
#输入用户名
ids[0].send_keys('wuya')
#输入密码
ids[1].send keys('admin')
#点击"登录"按钮
ids[2].click()
driver.quit()

4. By类的分析

如果在定位元素属性中包含了如 ID 等元素属性,那么在一个测试中,元素定位具体有哪几种方式,可以在by模块中的By类中看到。By类的代码如下:

class By(object):
"""
Set of supported locator strategies. 
"""ID = "id"
XPATH = "xpath"
LINK_TEXT = "link text"
PARTIAL_LINK_TEXT = "partial link text"
NAME = "name"
TAG_NAME = "tag name"
CLASS_NAME = "class name"
CSS_SELECTOR = "css selector"

注解:在By 类中,类属性实际就是元素定位的方式,经常使用的有 ID、NAME等。

5. iframe元素定位实战

在自动化测试中,如果无法定位到一个元素,那么最大的可能是定位的元素属性在iframe 框架中。iframe 对象代表一个HTML 的内联框架,在HTML 中iframe每出现一次,一个iframe对象就会被创建。

1)处理未嵌套的iframe

iframe 存在嵌套和未嵌套的页面。首先来看未嵌套的页面,如图所示。

实现以上截图效果的HTML代码如下。

其中frame.html代码为: 

frame-1.html代码为: 

frame-2.html代码如下: 

想要获取iframe框架中的“无涯课堂”,实现的代码如下:

#!/usr/bin/env python 
#-*-coding:utf-8-*-from selenium import webdriver
from selenium.webdriver.common.by import Bydriver=webdriver.Firefox()
driver.maximize_window()
driver.implicitly_wait(30)
#index.html的地址依据自己本地实际地址
driver.get('file://D:/git/github/book/html/frame/frame.html')
#获取"无涯课堂"文本信息
print driver.find_element_by_xpath('/html/body/center/font').text 
driver.quit()

运行以上代码后,会出现如下的错误:

selenium.common.exceptions.NoSuchElementException:Message:Unable to locate element:/html/body/center/font

依据给出的错误信息可以发现这个错误是元素定位错误导致的。由于存在iframe框架,首先需要进入到iframe框架,再定位iframe框架中的元素。定位的方式分为两种,一种是以ID的方式,另外一种是索引的方式。

以ID的方式定位时的实现代码: 

#!/usr/bin/env python 
#-*-coding:utf-8-*-from selenium import webdriverdriver=webdriver.Firefox()
driver.maximize_window()
driver.implicitly_wait(30)
#index.html的地址依据自己本地实际地址
driver.get('file:///D:/git/github/book/html/frame/frame.html')
#依据iframe的Id进入到frame框架
driver.switch_to_frame('text')
#获取"无涯课堂"文本信息
print driver.find_element_by_xpath('/html/body/center/font').text 
driver.quit()

也可以通过索引的方式进入到 iframe 框架中,然后再定位对应页面的元素属性。在以上的实例中,只有一个iframe,那么它的索引就是0,实现的代码如下:

#!/usr/bin/env python 
#-*-coding:utf-8-*-
from selenium import webdriverdriver=webdriver.Firefox()
driver.maximize window()
driver.implicitly_wait(30)
#index.html的地址依据自己本地实际地址
driver.get('file://D:/git/git/github/book/html/frame/frame.html')
#依据索引进入到frame框架
driver.switch_to_frame(0)
#获取"无涯课堂"文本信息
print driver.find_element_by_xpath('/html/body/center/font').text 
driver.quit()

2)处理嵌套的iframe

下面来看嵌套的iframe 在自动化测试中如何进行元素定位,以及如何跳出嵌套。嵌套页面的效果如图所示。

图中可以看到在一个页面里面嵌套了Bing 搜索的首页,该页面的HTML代码如下: 

在该页面要实现 Bing的搜索效果,首先需要进入到 iframe框架中,然后再定位Bing首页搜索输入框的元素属性。该iframe的ID是son,Bing首页搜索输入框的元素属性 ID是 sb_form_q。

实现在Bing首页搜索输入框中输入搜索关键字的代码如下:

#!/usr/bin/env python 
#-*-coding:utf-8-*-from selenium import webdriverdriver=webdriver.Firefox()
driver.maximize_window()
driver.implicitly_wait(30)
#bing.html的地址依据自己本地实际地址
driver.get('file:///C:/Users/Administrator/Desktop/san/san/130/frame/bing.html')
#依据索引进入到frame框架
driver.switch_to_frame('son')
#输入搜索关键字Selenium
driver.find_element_by_id('sb_form_q').send_keys('Selenium')
driver.quit()

下面是一个多层级的嵌套页面在自动化测试中的应用实例:

HTML代码如下:

在以上 HTML代码中,可以看到 Bing首页的搜索多了二层嵌套,页面层级结构如图所示。 

要想在Bing 首页搜索输入框中输入搜索关键字 Selenium,然后在下面的用户名输入框中输入Selenium,实现的代码如下: 

#!/usr/bin/env python
#-*-coding:utf-8-*-from selenium import webdriver
driver=webdriver.Firefox()
driver.maximize_window()
driver.implicitly_wait(30)
#second.html的地址依据自己本地实际地址
driver.get('file:///D:/git/git/github/book/html/frame/second.html')
#依据ID进入到第一层frame框架中
driver.switch_to_frame('parent')
#依据ID进入到第二层frame框架中
driver.switch_to_frame('son')
#输入搜索关键字 Selenium
driver.find_element_by_id('sb form_q').send_keys('Selenium')
#跳出frame框架
driver.switch_to_default_content()
#用户名输入框中输入Selenium
driver.find_element_by_name('username').send_keys('Selenium')
driver.quit()

17、Selenium页面交互实战

1. WebDriver浏览器的属性

WebDriver 提供了很多属性来支持对浏览器的操作,例如,获取测试地址、多窗口的处理、获取浏览器名称等,具体介绍如下。

1)获取测试的地址

获取测试的地址用到的方法是current_url,也就是获取当前测试的具体URL。以百度首页为例,获取的地址就是百度首页的地址,实现的测试代码如下:

#!/usr/bin/env python 
#-*-coding:utf-8-*-from selenium import webdriverdriver=webdriver.Firefox()
driver.maximize_window()
driver.get('http://www.baidu.com')
driver.implicitly_wait(30)
print('测试地址为∶{0}'.format(driver.current url))
driver.quit()

运行以上代码后获取的就是百度首页的地址。

2)获取当前页面代码

获取当前测试页面的代码用到的方法是 page_source。以百度首页为例,要想获取百度首页的页面代码,实现的代码如下:

#!/usr/bin/env python 
#-*-coding:utf-8-*-from selenium import webdriver
driver=webdriver.Firefox()
driver.maximize_window()
driver.get('http://www.baidu.com')
driver.implicitly_wait(30)
print('页面代码如下:(0)'.format(driver.page_source))
driver.quit()

3)获取当前的Title

获取当前的Title,即获取当前测试页面的标题。例如,百度首页的Title 是“百度一下,你就知道”。获取Title的测试代码为:

#!/usr/bin/env python 
#-*-coding:utf-8-*-
#author:wuyafrom selenium import webdriver
driver=webdriver.Firefox()
driver.maximize_window()
driver.get('http://www.baidu.com')
driver.implicitly_wait(30)
print('百度首页的Title为∶{0}'.format(driver.title))
driver.quit()

4)页面的前进和后退

前进用到的方法是forward,后退用到的方法是back。以百度首页和Bing搜索首页为例,要实现先打开百度首页,再打开 Bing 搜索页,再后退到百度首页,然后再从百度首页返回到Bing搜索页。

实现的测试代码为:

#!/usr/bin/env python 
#-*-coding:utf-8-*-from selenium importwebdriver 
import time as t
driver=webdriver.Firefox()
driver.maximize_window()
driver.get('http://www.baidu.com')
t.sleep(2)
driver.get('http://www.bing.com')
t.sleep(2)
#返回到百度
driver.back()
print('当前URL为∶{0}'.format(driver.current_url))
t.sleep(2)
#前进到bing 
driver.forward()
print('当前URL为∶{0}'.format(driver.current url))
driver.quit()

5)关闭程序

在Selenium中,quit 方法用来退出驱动程序(Driver)并关闭执行的浏览器;而close方法用来关闭执行的浏览器,所以关闭程序建议使用quit方法。

6)加载测试地址

在UI自动化测试中,打开测试地址用到的方法是get方法,它的参数是要打开的测试页面的地址。例如,要测试打开新浪邮箱的地址(https://mail.sina.com.cn/),测试代码如下:

#!/usr/bin/env python 
#-*-coding:utf-8-*-from selenium import webdriver
driver=webdriver.Firefox()
driver.maximize_window()
driver.get('http://mail.sina.com.cn/')
driver.quit()

7)多窗口实战

在新浪邮箱的登录页面(简称新浪登录页面,或登录页面)点击“注册”按钮,如何在打开的注册页面输入框中输入注册信息呢?这里会用到窗口处理的方法。current_window_handle用来获取当前浏览器的窗口句柄,window_handles用来获取浏览器的所有窗口句柄。

要实现在新浪登录页面点击注册,在注册页面邮箱地址输入框中输入邮箱地址,再次跳转到登录页面,代码如下:

#!/usr/bin/env python 
#-*-coding:utf-8-*-from selenium import webdriver 
import time as t
driver=webdriver.Firefox()
driver.maximize_window()
driver.get('http://mail.sina.com.cn/')
driver.implicitly_wait(30)
#获取当前窗口句柄
now_handle=driver.current_window_handle 
t.sleep(2)
#点击注册链接
driver.find_element_by_link_text('注册').click()
t.sleep(2)
#获取所有窗口句柄
handles=driver.window_handles 
#对所有窗口句柄循环处理
for handle in hancles:#判断handle不是当前窗口句柄if handle!=now handle:driver.switch_to_window(handle)t.sleep(2)driver.find_element_by_name('email').send_keys('yyds')t.sleep(2)# 关闭注册页面driver.close()
#切换到登录页面
driver.switch_to_window(now_handle)
t.sleep(3)
#在账号输入框中输入邮箱
driver.find_element_by_id('freename').send_keys('yyds')
t.sleep(4)
driver.quit()

8)浏览器最大化

使浏览器最大化的方法是 maximize_window。一般打开浏览器时,界面并不是最大化的,这对 UI 自动化测试的影响比较大,所以建议在打开浏览器后,调用该方法让浏览器最大化,实现的测试代码如下:

#!/usr/bin/env python 
#-*-coding:utf-8-*-from selenium import webdriver
driver=webdriver.Firefox()
#浏览器最大化
driver.maximize_window()
driver.get('http://www.baidu.com')
driver.quit()

9)刷新

刷新用到的方法是refresh方法。在UI自动化测试中,某些场景需要用到页面的刷新。例如,打开百度首页后,输入搜索关键字,然后刷新页面查看效果,实现的代码如下:

#!/usr/bin/env python 
#-*-coding:utf-8-*-from selenium import webdriver 
import time as tdriver=webdriver.Firefox()
driver.maximize_window()
driver.get('http://www.baidu.com')
driver.find_element_by_id('kw').send_keys('Selenium')
t.sleep(2)
#对页面进行刷新
driver.refresh()
t.sleep(3)
driver.quit()

10)获取执行的浏览器

获取执行的浏览器名称用到的是name方法,实现的代码如下:

#!/usr/bin/env python 
#-*-coding:utf-8-*-from selenium import webdriver 
import time as t
driver=webdriver.Firefox()
driver.maximize_window()
driver.get('http://www.baidu.com')
#测试使用的浏览器
print('测试执行的浏览器为{0}'.format(driver.name))
driver.quit()

2. WebElement类的方法

WebElement 类中有很多方法可以应用在UI 自动化测试中。例如,get_attribute方法用以获取输入框中的Value值,is_enabled方法用来判断文本是否可编辑。下面具体介绍这些方法在UI自动化测试中的应用。

1)清空

在Selenium 中,实现清空用到的方法是 clear。例如,在百度搜索输入框中输入搜索关键字 Selenium,然后再调用 clear 方法清空输入的关键字,实现的代码如下:

#!/usr/bin/env python 
#-*-coding:utf-8-*-from selenium import webdriver 
import time as t
driver=webdriver.Firefox()
driver.maximize_window()
driver.get('http://www.baidu.com')
so=driver.find_element_by_id('kw')
#输入搜索的关键字
so.send keys('Selenium')t.sleep(2)
#清空搜索的关键字
so.clear()
t.sleep(2)
driver.quit()

2)获取元素属性值

get_attribute 方法可用来获取元素属性的值。在测试中,经常需要获取输入框中的值,或者获取输入框中的提示信息。仍以新浪邮箱的登录页页为例,实现的代码如下:

接下来测试用户名输入框是否显示“请输入用户名”的提示信息。“请输入用户名”的元素属性是placeholder,实现的代码如下: 

#!/usr/bin/env python 
#-*-coding:utf-8-*-from selenium import webdriver 
import time as t
driver=webdriver.Firefox()
driver.maximize_window()
driver.get('file:///D:/git/Python/ActualCombat/Chapter2/html/attribute.h tml')
name=driver.find_element_by_name('username')
print('用户名输入框中提示信息为:{0}'.format(name.get_attribute('placeholder')))
t.sleep(2)
driver.quit()

在输入框中输入的值一般都在Value属性中,例如,在百度搜索输入框中输入的搜索关键字存放在百度搜索 input的Value属性中。要想获取该关键字,实现的代码如下:

#!/usr/bin/env python 
#-*-coding:utf-8-*-from selenium import webdriver 
import time as t
driver=webdriver.Firefox()
driver.maximize_window()
driver.get('http://www.baidu.com')
so=driver.find_element_by_id('kw')
so.send_keys('Selenium WebDriver')
print('百度搜索输入框中填写的关键字为∶{0}'.format(so.get_attribute('value'))
driver.quit()

注解:运行以上代码后,会显示在搜索框输入的搜索关键字。

3)检查元素是否可见

is_displayed 方法用于测试该属性对用户是否可见,返回结果为布尔类型。如果可见,返回的结果为 True;如果不可见,返回的结果为 False。

这里以百度首页为例,测试“关于百度”是否可见,实现的测试代码如下: 

#!/usr/bin/env python 
#-*-coding:utf-8-*-from selenium import webdriver 
import time as t
driver=webdriver.Firefox()
driver.maximize window()
driver.get('https://www.baidu.com/')
about=driver.find_element_by_link_text('关于百度')
print('关于百度是否可见{0}'.format(about.is_displayed())
driver.quit()

注解:由于“关于百度”在页面上是可见的,那么运行代码后返回的结果应该是True,如图所示。

4)检查元素是否可编辑

is_enabled 方法用于测试文本是否可编辑,返回的结果是布尔类型。如果可编辑,返回的结果是 True;如果不可编辑,返回的结果是 False。以百度搜索输入框为例,因为它是可编辑的,所以返回的结果是 True,实现的测试代码如下: 

#!/usr/bin/env python 
#-*-coding:utf-8-*-from selenium import webdriver 
import time as t
driver=webdriver.Firefox()
driver.maximize window()
driver.get('https://www.baidu.com/')
so=driver.find_element_by_id('kw')
print('搜索输入框是否可编辑∶{0}'.format(so.is_enabled())
driver.quit()

执行后的结果如图所示:

5)是否已选中

is_selected 方法主要针对单选按钮或者复选按钮,判断它们是否已被选中,返回结果是布尔类型。如果选中,返回的结果是 True;如果未选中,返回的结果是 False。这里以新浪邮箱的登录为例,验证登录页面是否默认选中了自动登录,实现的代码如下: 

#!/usr/bin/env python 
#-*-coding:utf-8-*-from selenium import webdriver
import time as tdriver=webdriver.Firefox()
driver.maximize_window()
driver.get('http://mail.sina.com.cn/')
autoLogin=driver.find_element_by_id('storel')
print('新浪登录页面自动登录是否已默认选中∶{0}'.format(autoLogin.is_selected()))
driver.quit()

注解:新浪登录页面的自动登录已默认选中,所以运行以上代码成功后,会返回True,如图所示。

6)提交表单

提交表单用到的方法是submit。例如,在输入框中输入“昵称”,点击“提交”按钮,跳转到另外一个页面,Form表单的HTML代码如下: 

点击“提交”按钮后跳转到index.html页面,代码如下: 

实现以上交互的代码如下: 

#!/usr/bin/env python 
#-*-coding:utf-8-*-from selenium import webdriver 
import time as tdriver=webdriver.Firefox()
driver.maximize_window()
driver.get('file://D:/git/Python/ActualCombat/Chapter2/html/submit.html')
driver.find_element_by_name('username').send keys('WuYa')
driver.find_element_by_name('form').submit()
t.sleep(2)
driver.quit()

3. 下拉框实战

1)Select类的详解

在UI 自动化测试中,经常会遇到下拉框的应用。针对下拉框,Selenium 提供了Select类来处理,Select类在select模块中。使用Select类首先需要导入,导入方式是 from selenium.webdriver.support.select import Select。在Select类中,构造方法的参数是 webelement,检查指定的元素时,如果参数错误就会抛出UnexpectedTagNameExpection的异常错误信息。在Select类中提供了很多方法可在下拉框定位中使用,下面具体介绍这些方法的应用。

2)下拉框定位的思路

这里以在百度搜索设置中设定期望搜索结果显示的条数为例,说明下拉框操作方式,如图所示。

这部分的HTML代码如下: 

下拉框的元素属性ID是nr,在图3-3-1中,实现选择下拉框选项内容的步骤为:

  1. 首先定位到Select下拉框的元素属性,具体代码是nr=driver.find_element_by_id(‘nr')。
  2. 实例化Select类,参数为nr,具体代码为select=Select(nr)。
  3. Select实例化后的对象select可以调用Select类的任何一个方法。

3)索引定位实战

使用索引的方式实现下拉框选项的选择,用到的方法是 select_by_index。该方法的参数是索引值。例如,想要实现每页显示50条,它在第三位,由于索引是从0开始的,所以索引就是2,实现的代码如下: 

#!/usr/bin/env python 
#-*-coding:utf-8-*-from selenium import webdriver
from selenium.webdriver.support.select import Select
from selenium.webdriver.common.action_chains import ActionChains 
import time as tdriver=webdriver.Firefox()
driver.maximize_window()
driver.implicitly_wait(30)
driver.get('http://www.baidu.com')
#实现鼠标悬浮到百度首页的设置
element=driver.find_element_by_css_selector('a.pf:nth-child(8)')
t.sleep(3)
ActionChains(driver).move_to_element(element).perform()
t.sleep(3)
#点击设置中的搜索设置按钮
driver.find_element_by_css_selector('.setpref').click()
t.sleep(2)
#定位到下拉框的元素属性
nr=driver.find_element_by_name('NR')
#实例化Select类
select=Select(nr)
select.select_by_index(2)
print('下拉框选择的最新条数是∶', nr.get_attribute('value'))
t.sleep(3)
driver.quit()

4)value定位实战

通过 Value 值可定位下拉框中的选项,用到的方法是 select_by_value。该方法的参数是下拉框中具体的Value值,需要选择下拉框中的哪个值,直接在该方法的参数中填写具体的Value值即可。本例中的Value值是10、20、50,这里选择在下拉框中每页显示20条,则对应的Value值是20,实现的代码如下:

#!/usr/bin/env python 
#-*-coding:utf-8-*-from selenium import webdriver
from selenium.webdriver.support.select import Select
from selenium.webdriver.common.action chains import ActionChains 
import time as tdriver=webdriver.Firefox()
driver.maximize_window()
driver.implicitly_wait(30)
driver.get('http://www.baidu.com')
#实现鼠标悬浮到百度首页的设置
element=driver.find_element_by_css_selector('a.pf:nth-child(8)')
t.sleep(3)
ActionChains(driver).move_to_element(element).perform()
t.sleep(3)
#点击设置中的搜索设置按钮
driver.find_element_by_css_selector('.setpref').click()
t.sleep(2)
#定位到下拉框的元素属性
nr=driver.find_element_by_name('NR')
#实例化Select类
select=Select(nr)
#按value值的方式来选择下拉框中的内容
select.select_by_value('20')
print('下拉框选择的最新条数是∶', nr.get_attribute('value'))
t.sleep(3)
driver.quit()

5)文本定位实战

使用文本的方式实现对下拉框中选项的选择,用到的方法是 select_by_visible_text,参数是具体的text 值。本例中搜索设置下拉框的文本信息具体为“每页显示10条”“每页显示20条”“每页显示50条”。

这里选择在下拉框中每页显示50条,那么调用的时候,实际参数就是“每页显示50条”,实现该测试点的代码如下:

#!/usr/bin/env python 
#-*-coding:utf-8-*-from selenium import webdriver
from selenium.webdriver.support.select import Select
from selenium.webdriver.common.action chains import ActionChains 
import time as t
driver=webdriver.Firefox()
driver.maximize_window()
driver.implicitly_wait(30)
driver.get('http://www.baidu.com')
#实现鼠标悬浮到百度首页的设置
element=driver.find_element_by_css_selector('a.pf:nth-child(8)')
t.sleep(3)
ActionChains(driver).move_to_element(element).perform()
t.sleep(3)
#点击设置中的搜索设置按钮
driver.find_element_by_css_selector('.setpref').click()
t.sleep(2)
#定位到下拉框的元素属性
nr=driver.find_element_by_name('NR')
#实例化Select类
select=Select(nr)
#按value值的方式来选择下拉框中的内容
select.select_by_visible_text('每页显示50条')
print('下拉框选择的最新条数是∶', nr.get_attribute('value'))
t.sleep(3)
driver.quit()

4. 弹出框实战

1)Alert类的详解

在UI自动化测试中经常会遇到Alert弹出框的场景。在HTML的Javascript交互中主要有 Alert 警告框、Confirm 确认框、Prompt 消息对话框。

在Selenium中对于弹出框的处理方可以使用alert模块中的Alert类,在Alert类中,方法text用来获取Alert弹出框的文本信息。accept和dismiss方法主要应用在Confirm弹出确认框中,accept用来接受确认框,dismiss用来拒绝确认框。send_keys主要应用在Prompt 消息对话框中,在输入框中输入要输入的值。在Selenium 中是通过switch_to_alert 方法调用 Alert 类中的方法,而 switch_to_alert 方法是属于WebDriver类的方法。

2)警告框的处理

这里以百度搜索设置为例,在进行搜索设置后点击“保存设置”按钮,弹出alert对话框,点击“确定”按钮,如图所示。

要想点击“保存设置”按钮后,获取 Alert 弹出框的文本信息,点击弹出框中的“确定”按钮,实现的代码如下:

#!/usr/bin/env python 
#-*-coding:utf-8-*-from selenium import webdriver
from selenium.webdriver.common.alert import Alert
from selenium.webdriver.common.action chains import ActionChains 
import time as t
driver=webdriver.Firefox()
driver.maximize_window()
driver.implicitly_wait(30)
driver.get('http://www.baidu.com')
#实现鼠标悬浮到百度首页的设置
element=driver.find_element_by_css_selector('a.pf:nth-child(8)')
t.sleep(3)
ActionChains(driver).move_to_element(element).perform()
t.sleep(3)
#点击设置中的搜索设置按钮
driver.find_element_by_css_selector('.setpref').click()
t.sleep(2)
#点击搜索设置按钮
driver.find_element_by_css_selector('#gxszButton > a.prefpanelgo').click()
t.sleep(2)
#获取弹出框的文本信息
print('alert 弹出框的文本信息为:{0}'.format(driver.switch_to_alert().text))
#点击alert弹出框中的确定按钮
driver.switch_to_alert().accept()

运行以上代码后,会打印出Alert的文本信息。

3)确认框的处理

Confirm 是弹出确认的对话框,用户可以选择“确定”或者“取消”按钮。点击“确定”按钮调用的方法是accept,点击“取消”按钮调用的方法是dismiss,如图所示。

这部分的HTML交互代码如下:

实现点击“确定”按钮的代码如下: 

#!/usr/bin/env python 
#-*-coding:utf-8-*-from selenium import webdriver
from selenium.webdriver.common.alert import Alert 
import time as tdriver=webdriver.Firefox()
driver.maximize_window()
driver.implicitly_wait(30)
driver.get('file://D:/git/Python/ActualCombat/Chapter2/html/confirm.htm 1')
#点击按钮
driver.find_element_by_css_selector('body > center >input[type="button"]').click()
t.sleep(2)
#点击“确定”按钮
driver.switch_to_alert().accept()
t.sleep(2)
driver.quit()

4)消息对话框的处理

Prompt消息对话框,在JavaScript中用于询问一些需要与用户交互的信息。要想在Prompt消息对话框中输入信息,用到的方法是 send_keys,如图所示。 

实现的HTML代码如下: 

在弹出的输入框中输入姓名后,点击“确定”按钮。实现的测试代码如下:

#!/usr/bin/env python 
#-*-coding:utf-8-*-from selenium import webdriver
from selenium.webdriver.common.alert import Alert 
import time as t
driver=webdriver.Firefox()
driver.maximize_window()
driver.implicitly_wait(30)
driver.get('file:///D:/git/Python/ActualCombat/Chapter2/html/prompt.html ')
#点击按钮
driver.find_element_by_css_selector('body > center > input[type="button"]').click()
t.sleep(2)
#在弹出的输入框中输入姓名
driver.switch_to_alert().send_keys('无涯')
t.sleep(2)
driver.switch_to_alert().accept()
t.sleep(3)
driver.quit()

5. WebDriverWait类实战

在UI 自动化测试中,首先要保障测试脚本的稳定运行。但是在实际的测试场景中,由于网络的因素导致需要测试的页面打不开或者打开延迟,从而导致页面元素找不到等各种错误的出现。

在UI自动化测试中,等待主要存在如下三种形式,分别是:

  1. 固定等待。如调用time模块中的sleep方法,固定等待几秒。
  2. 隐式等待。用到的方法是 implicitly_wait,隐藏式等待指设置最长等待时间。
  3. 显式等待。主要指程序会每隔一段时间执行自定义的程序判断条件,如果判断条件成功,程序就继续执行;如果判断条件失败,程序就会报TimeOutEcpection的异常信息。

例如,测试百度首页的搜索时,设置固定等待几秒的时间,实现的代码如下:

#!/usr/bin/env python 
#-*-coding:utf-8-*-from selenium import webdriver 
import time as t
driver=webdriver.Firefox()
driver.maximize_window()
driver.get('http://www.baidu.com')
t.sleep(3)
driver.find_element_by_id('kw').send_keys('Selenium')
driver.quit()

在以上测试代码中,增加了固定等待3秒的时间,试想,如果每个步骤都增加等待3秒的时间,那么一个测试用例执行下来后,等待的时间就有十几秒,这样会大大地延长测试用例执行的时间。因此,在Selnium 中提供了WebDriverWait类,专门解决网络延迟等待等问题。

1)WebDriverWait类详解

在学习 WebDriverWait类应用之前,先来了解一下 WebDriverWait类的相关概念。WebDriverWait类放置在wait模块下,使用该类的时候,首先需要导入并且对它进行实例化,导入的代码如下:

from selenium.webdriver.support.ui import WebDriverWait

WebDriverWait类的参数分别是driver和timeout。driver指的是webdriver实例化后的对象,timeout指的是具体等待的时间。在WebDriverWait类的until方法中,参数method指的是需要调用的方法,调用的方法来自expected_conditions模块中的类,也就是说 WebDriverWait会与 expected_conditions模块结合起来应用。调用 expected_conditions 模块中的类之后,一般会返回两种形式的对象实例:一种是布尔类型,返回 True 程序继续执行,如果不是 True,程序就会报TimeOutExpection 的异常信息;另外一种是返回某个类的具体实例对象,使用该对象就可以直接调用该类中的方法。下面具体看一下该模块中的类在UI 自动化测试中的应用。

2)元素可见并且可操作

element_to_be_clickable 判断某元素可见后执行输入、点击等操作。element_to_be_clickable 满足条件后返回的是 WebElement 类的实例对象,这样就可以调用 WebElement类中的send_keys等方法。这里以百度搜索输入框为例,打开百度首页后首先需要加载搜索的输入框,然后才可以在搜索输入框中输入搜索的关键字,否则就返回错误的信息。

实现点击百度首页的“百度一下”按钮的代码如下:

#!/usr/bin/env python 
#-*-coding:utf-8-*-from selenium import webdriver
from selenium.webdriver.support.ui import WebDriverWait 
from selenium.webdriver.support import expected_conditions 
from selenium.webdriver.common.by import By 
import time as tdriver=webdriver.Firefox()
driver.maximize_window()
driver.get('http://www.baidu.com')
driver.implicitly_wait(30)
so=WebDriverWait(driver,10).until(expected_conditions.element_to_be_clickable((By.ID,'kw')))
so.send_keys('Selenium')
driver.quit()

注解:在以上代码中,我们可以看到 WebDriverWait直接调用静态方法 until,并且显式等待10 s的时间,在until的方法中又调用了expected_conditions模块中的element_to_be_clickable类,在element_to_be_clickable类中需要指定按ID或者其他元素属性来指定元素的属性。

运行以上代码后,在10 s 范围内,只要百度首页的搜索输入框可以正确地加载出来,都是可以在输入框中输入搜索的关键字。对以上代码进行修改,故意写错元素属性,把 kw修改成 kwkw,执行后,就会提示 TimeOutExpection的错误提示,如图所示。

3)指定元素的文本位置

text_to_be_present_in_element 指定元素的文本位置,一般用于验证一个文本信息或者错误的提示信息。在这里以新浪邮箱登录页面为例,当账号和密码为空,点击“登录”按钮,验证页面返回的错误提示信息是否为“请输入邮箱名”,实现的代码如下:

#!/usr/bin/env python 
#-*-coding:utf-8-*-from selenium import webdriver
from selenium.webdriver.support.ui import WebDriverWait 
from selenium.webdriver.support import expected_conditions 
from selenium.webdriver.common.by import By 
import time as tdriver=webdriver.Firefox()
driver.maximize window()
driver.implicitly_wait(30)
driver.get('https://mail.sina.com.cn/#')
#输入新浪邮箱账号
driver.find_element_by_id('freename').send_keys('')
#输入新浪邮箱密码
driver.find element_by_id('freepassword').send_keys('')
#点击新浪邮箱登录按钮
driver.find element_by_link_text('登录').click()
WebDriverWait(driver,10).until(expected_conditions.text_to_to_be_present_in_element((By.XPATH,'/html/body/div[3]/div/div[2]/div/div/div[4]/div[1]/div[1]1/div[1]/span[1]'),'请输入邮箱名'))
driver.quit()

注解:在以上代码中,如果错误提示信息是“请输入邮箱名”,程序执行正确;如果不是,就会提示对应的错误信息。text_to_be_present_in_element 返回的结果是True或者False,当返回结果是True的时候,执行结果是正确的;当返回的结果是False的时候,则会提示TimeOutExpection的错误信息。

因为 text_to_be_present_in_element返回的结果是 True或者 False,对以上代码进行修改,见打印的结果:

#!/usr/bin/env python 
#-*-coding:utf-8-*-from selenium import webdriver
from selenium.webdriver.support.ui import WebDriverWait 
from selenium.webdriver.support import expected_conditions 
from selenium.webdriver.common.by import By 
import time as tdriver=webdriver.Firefox()
driver.maximize_window()
driver.implicitly_wait(30)
driver.get('https://mail.sina.com.cn/#')
#输入新浪邮箱账号
driver.find_element_by_id('freename').send_keys('')
#输入新浪邮箱密码
driver.find_element_by_id('freepassword').send_keys('')
#点击新浪邮箱登录按钮
driver.find_element_by_link_text('登录').click()
isText=WebDriverWait(driver,10).until(expected_conditions.text_to_be_present_in_element((By.XPATH,'/html/body/div[3]/div/div[2]/div/div/div[4]/div[1]/div[1]/div[1]/span[1]'),'请输入邮箱名')) 
print('打印结果是否是True∶{0}'.format(isText))
driver.quit()

执行后的结果:

4)判断元素是否可见

visibility_of_element_located方法的作用是等元素可见后再执行操作。这里以百度首页的“关于百度”为案例,如果“关于百度”可见,再点击百度首页的“关于百度”链接。

实现的代码如下: 

#!/usr/bin/env python 
#-*-coding:utf-8-*-from selenium import webdriver
from selenium.webdriver.support.ui import WebDriverWait 
from selenium.webdriver.support import expected_conditions 
from selenium.webdriver.common.by import By 
import time as tdriver=webdriver.Firefox()
driver.maximize_window()
driver.implicitly_wait(30)
driver.get('http://www.baidu.com')
aboutBaidu=WebDriverWait(driver,10).until(expected_conditions.visibility_of_element_located((By.LINK_TEXT,'关于百度'))
aboutBaidu.click()
driver.quit()

6. ActionChains类实战

在功能的自动化测试中,经常会用到鼠标事件。在Selenium 中,主要在action_chains模块的ActionChains类中,导入的方式为:

from selenium.webdriver.common.action_chains import ActionChains

1)ActionChains类详解

在ActionChains类中提供了对常用鼠标操作的方法,例如,鼠标悬浮到某一元素等操作。要使用ActionChains类中的方法首先需要对ActionChains类进行实例化,该类的构造函数参数为 driver,也就是 WebDriver 实例化后的对象,对ActionChinas类实例化后可以调用它里面的方法,实例化该类的代码如下:

#!/usr/bin/env python
#-*-coding:utf-8-*-from selenium import webdriver 
import time as t
from selenium.webdriver.common.action_chains import ActionChainsdriver=webdriver.Firefox()
driver.maximize_window()
driver.implicitly_wait(30)
driver.get('http://www.baidu.com')
#对ActionChains类进行实例化
actionChains=ActionChains(driver)
driver.quit()

对 ActionChains 类实例化后,输入对象 actionChains,按下鼠标,就会显示该类中的方法,如图所示。

2)鼠标悬浮操作

move_to_element 方法用来使鼠标悬浮到某一元素上。这里以百度首页的搜索设置为例,要想选择“搜索设置”选项,首先需要鼠标悬浮到“设置”选项上,显示“搜索设置”后才可以点击,如图所示。 

要想实现拖动鼠标悬浮到“设置”,再点击“搜索设置”按钮,测试代码如下:

#!/usr/bin/env python 
#-*-coding:utf-8-*-from selenium import webdriver 
import time as t
from selenium.webdriver.common.action_chains import ActionChainsdriver=webdriver.Firefox()
driver.implicitly_wait(30)
driver.get('http://www.baidu.com')
#对ActionChains类进行实例化
actionChains=ActionChains(driver)
locator=driver.find_element_by_css_selector('#ul> a.pf')
#鼠标悬浮到设置
actionChains.move_to_element(locator).perform()
#点击搜索设置
driver.find_element_by_xpath('/*[@id="wrapper"]/div[6]/a[1]').click()
driver.quit()

运行以上代码就可以看到,鼠标移动到“设置”选项,显示“搜索设置”选项后,点击“搜索设置”按钮,并跳转到“搜索设置”页面。

3)鼠标右键操作

content_click 是触发鼠标右键操作的方法。这里以百度首页为例,要想用鼠标在搜索输入框上点击右键时,弹出鼠标右键的交互信息,实现的代码如下:

#!/usr/bin/env python 
#-*-coding:utf-8-*-from selenium import webdriver 
import time as t
from selenium.webdriver.common.action chains import ActionChainsdriver=webdriver.Firefox()
driver.implicitly_wait(30)
driver.get('http://www.baidu.com')
#对ActionChains类进行实例化
actionChains=ActionChains(driver)
so=driver.find_element_by_id('kw')
#鼠标在百度搜索输入框右键操作
actionChains.context_click(so).perform()
t.sleep(6)
driver.quit()

4)鼠标双击操作

double_click 是触发鼠标双击操作的方法,一般使用在有数据交互的地方。例如,有这样一个业务,点击按钮会向数据库中插入一条数据,而且只能插入一条数据,如果前端对双击没有进行处理的话,双击按钮后就会向数据库插入两条数据。这里以百度首页的“百度一下”按钮为例,演示双击事件在自动化测试中的应用。实现的代码如下:

#!/usr/bin/env python 
#-*-coding:utf-8-*-from selenium import webdriver 
import time as t
from selenium.webdriver.common.action_chains import ActionChainsdriver=webdriver.Firefox()
driver.implicitly_wait(30)
driver.get('http://www.baidu.com')
#对ActionChains类进行实例化
actionChains=ActionChains(driver)
driver.find_element_by_id('kw').send_keys('Selenium')
locator=driver.find_element_by_id('su')
#对“百度一下”按钮双击操作
actionChains.double_click(locator).perform()
driver.quit()

7. 键盘事件实战

对于键盘事件的操作,Selenium提供了keys模块中的Keys类来处理,导入的代码为 from selenium.webdriver.common.keys import Keys。这里以百度搜索输入框为例,在搜索输入框中输入搜索的关键字,通过 Ctrl+A 全选之后,通过Ctrl+C复制。按下“Backspace”键删除输入的搜索关键字,然后打开 Bing首页,把复制的搜索关键字粘贴到Bing搜索输入框中,实现的代码如下:

#!/usr/bin/env python 
#-*-coding:utf-8-*-from selenium import webdriver 
import time as t
from selenium.webdriver.common.keys import Keysdriver=webdriver.Firefox()
driver.implicitly_wait(30)
driver.get('http://www.baidu.com')
so=driver.find_element_by_id('kw')
so.send_keys('Selenium')
#选中输入框的搜索关键字
so.send keys(Keys.CONTROL,'a')
#复制搜索的关键字
so.send_keys(Keys.CONTROL,'c')
#按下Backspace删除输入的搜索关键字
so.send_keys(Keys.BACKSPACE)
#打开bing 搜索的首页
driver.get('http://www.bing.com')
bingSo=driver.find_element_by_id('sb_form_q')
#复制搜索关键字到bing搜索的输入框中
bingSo.send_keys(Keys.CONTROL,'v')
driver.quit()

8. JavaScript的处理

Selenium也提供了对 JavaScript 的处理,例如,滑动到浏览器的底部或者顶端,对时间控件及对富文本等的处理,都需要 JavaScript 的脚本配合。在Selenium中对 JavaScript脚本调用的方法是 execute_script,下面结合具体的实例说明这部分在自动化测试中的应用。

1)浏览器滑动操作

在自动化测试中,特别是在数据查询的页面中,由于页面内容太多,经常需要点击浏览器的底部,但往往由于看不到底部导致找不到页面元素而失败。例如,在百度搜索中,搜索后想要前往下一页,需要滑动到浏览器底部才可以找到翻页按钮,然后点击。下面就以百度搜索为例,实现滑动到浏览器的底部或者顶部,代码如下:

#!/usr/bin/env python 
#-*-coding:utf-8-*-from selenium import webdriver 
import time as  tdriver=webdriver.Firefox()
driver.implicitly_wait(30)
driver.get('http://www.baidu.com')
driver.find_element_by_id('kw').send_keys('Selenium')
driver.find_element_by_id('su').click()
#浏览器滑动到底部js代码
down="var q=document.documentElement.scrollTop=10000"
t.sleep(3)
#操作js实现鼠标滑动到浏览器底部
driver.execute_script(down)
t.sleep(3)
#点击下一页
driver.find_element_by_link_text('下一页>').click()
t.sleep(3)
#浏览器滑动到顶部 js 代码
up="var q=document.documentElement.scrollTop=0"
#先滑动到底部
driver.execute script(down)
t.sleep(3)
#操作js实现鼠标滑动到浏览器顶部
driver.execute_script(up)
t.sleep(4)
driver.quit()

2)富文本的处理

在很多的Web 产品中,经常存在富文本的内容,特别是要求在富文本中写入内容。对于富文本的操作不能按照常规的元素定位方式去定位。富文本一般都在iframe框架中,所以需要通过iframe的ID或者索引的方式进入到iframe中,再通过 JaveScript 的方式在富文本中输入需要的内容。例如,要想在微信公众号中编写文章,那么首先需要进入到 iframe。

微信公众号富文本部分的HTML代码如图所示:

从图中可以看到,iframe的ID为ueditor_0,那么如果想要在该富文本里面写入内容,可单独写一个可调用函数,参数是编写的内容,代码如下: 

def richText(content):····往富文本里面写入内容····js="document.getElementById('ueditor_0').contentwindow." \"document.body.innerHTML='{0}'".format(content)driver.execute_script(js)

下面就以UEditor的富文本为例,实现在富文本中输入“Python自动化测试实战”的文字,UEditor要测试的页面如图所示。

实现的代码如下:

#!/usr/bin/env python
#-*-coding:utf-8-*-from selenium import webdriver 
import time as  tdef richText(content):""往富文本里面写入内容"js="document.getElementById('ueditor_0').contentWindow." \"document.body.innerHTML='{0}'".format(content)driver.execute_script(js)driver=webdriver.Firefox()
driver.implicitly_wait(30)
driver.get('http://ueditor.baidu.com/website/onlinedemo.html')
richText('Python自动化测试实战')
t.sleep(3)
driver.quit()

3)时间控件的处理

时间控件在测试中也是经常遇到的,特别是在查询中,需要按某一时间段查询出数据并且来验证查询功能是否正确。但是,大多数时间控件是只读属性,需要手动选择时间控件。在自动化测试中怎样自动输入时间呢?时间控件的UI 交互如图所示。

这部分的HTML代码如图所示:

要实现在只读属性的时间控件中输入时间,步骤为:

  1. 取消时间控件的只读属性。
  2. 取消只读属性后,在Input输入框中给Value赋值,填写需要输入的时间,结合以上步骤,在以上时间控件中,编写开始和结束时间的可调用函数。

代码如下: 

def startJs(startTime):'''开始时间中输入自定义的时间'''js="$(\"input[placeholder='开始时间≥当前时间']\").removeAttr('readonly');" \
"$(\"input[placeholder='开始时间≥当前时间']\").attr('value','{0}')".format(startTime)driver.execute_script(js)def endJs (endTime):'''结束时间中输入自定义的时间'''js="$(\"input[placeholder='结束时间>开始时间']\").removeAttr('readonly');" \
"$(\"input[placeholder='结束时间>开始时间']\").attr('value','{0}')".format(endTime)driver.execute_script(js)

要想在打开该页面后,在活动时间中输入开始时间和结束时间,实现的代码如下:

#!/usr/bin/env python 
#-*-coding:utf-8-*-from selenium import webdriver 
import time as tdef startJs(startTime):'''开始时间中输入自定义的时间'''js="$(\"input[placeholder='开始时间≥当前时间']\").removeAttr('readonly');" \
"$(\"input[placeholder='开始时间≥当前时间']\").attr('value','{0}')".format(startTime)driver.execute_script(js)def endJs (endTime):'''结束时间中输入自定义的时间'''js="$(\"input[placeholder='结束时间>开始时间']\").removeAttr('readonly');" \
"$(\"input[placeholder='结束时间>开始时间']\").attr('value','{0}')".format(endTime)driver.execute_script(js)driver=webdriver.Firefox()
driver.implicitly wait(30)
driver.get('file:///C:/Users/Administrator/Desktop/Time/Time/Tindex.html')
startJs('2018-01-01 00:00:00')
t.sleep(3)
endJs('2018-08-08 23:59:59')
driver.quit()

运行以上代码后,可看到在活动时间中实现了自定义输入开始时间和结束时间。

9. 获取截图

在自动化测试中,在测试执行期间获取到截图信息,一方面可以定位错误的脚本,方便调试错误代码;另外一方面也可以以此为据,与开发人员进行有效的沟通。

1)获取当前截图

save_screenshot 是获取当前截图的方法。以百度首页搜索为例,要想打开百度首页后获取百度首页的截图,实现的代码如下:

#!/usr/bin/env python
#-*-coding:utf-8-*-from selenium import webdriverdriver=webdriver.Firefox()
driver.maximize_window()
driver.get('http://www.baidu.com')
driver.implicitly_wait(30)
driver.save_screenshot('baidu.png')
driver.quit()

代码执行后会在当前目录下生成baidu.png的图片。

2)保存当前屏幕快照

get_screenshot_as_file 方法可以将当前的屏幕快照保存成.png 文件,保存文件的时候可以填写完整的文件路径。依然以百度首页为例,实现的代码如下:

#!/usr/bin/env python 
#-*-coding:utf-8-*-from selenium import webdriverdriver=webdriver.Firefox()
driver.maximize_window()
driver.get('http://www.baidu.com')
driver.implicitly_wait(30)
driver.get_screenshot_as_file('c:/baidu.png')
driver.quit()

运行代码后会在C盘下生成 baidu.png的图片,打开后显示的是百度的首页。

3)获取图片二进制数据

get_screenshot_as_png 方法用来获取截取图片的二进制数据。该方法在实际的测试场景中使用较少。以百度首页为例,实现的代码如下:

#!/usr/bin/env python 
#-*-coding:utf-8-*-from selenium import webdriver
driver=webdriver.Firefox()
driver.maximize_window()
driver.get('http://www.baidu.com')
driver.implicitly_wait(30)
print(driver.get_screenshot_as_png())
driver.quit()

三、数据驱动

1、ddt装饰器

在UI 自动化测试中,自动化测试的数据如何分离和高效的维护,页面元素又怎样分离和维护,都是自动化测试人员需要面对的难点问题。

ddt 是 Python 的第三方库。ddt 模块提供了创建数据驱动的测试,关于该模块详细的信息建议到官方查看,地址为:ddt · PyPI,安装的命令是:pip install ddt,如图所示。

ddt安装成功后,在Python的命令行环境中即可导入。

4 种使用模式:

  1. 引入的装饰器 @ddt
  2. 导入数据的 @data
  3. 拆分数据的 @unpack
  4. 导入外部数据的 @file_data

在ddt模块中,@data表示元组的列表数据,@unpack表示用来解压元组到多个参数。

测试程序:

from selenium import webdriver
import unittest, time
import logging, traceback
import ddt
from selenium.common.exceptions import NoSuchElementException# 初始化日志对象
logging.basicConfig(# 日志级别level = logging.INFO,# 日志格式# 时间、代码所在文件名、代码行号、日志级别名字、日志信息format = '%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s',# 打印日志的时间datefmt = '%a, %d %b %Y %H:%M:%S',# 日志文件存放的目录(目录必须存在)及日志文件名filename = 'report.log',# 打开日志文件的方式filemode = 'a'
)@ddt.ddt
class TestDemo(unittest.TestCase):def setUp(self):self.driver = webdriver.Chrome()self.driver.set_page_load_timeout(10)@ddt.data(["hello", "哈喽"],["hiphop", "嘻哈"],["morning", "早晨"])@ddt.unpack  # 将测试数据解包对应到 testdata 和 expectdata;即该测试方法会执行3次def test_dataDrivenByObj(self, testdata, expectdata):url = "https://www.iciba.com"# 访问百度首页self.driver.get(url)# 设置隐式等待时间为5秒#self.driver.implicitly_wait(5)try:# 找到搜索输入框,并输入测试数据self.driver.find_element_by_xpath("//input[@type='search']").send_keys(testdata)# 找到搜索按钮,并点击self.driver.find_element_by_xpath('//input[@placeholder="请输入您要翻译的单词"]/following-sibling::div').click()time.sleep(5)# 断言期望结果是否出现在页面源代码中self.assertTrue(expectdata in self.driver.page_source)except NoSuchElementException as e:logging.error("查找的页面元素不存在,异常堆栈信息:" + str(traceback.format_exc()))raise NoSuchElementExceptionexcept AssertionError as e:logging.info("搜索“%s”,期望“%s”,失败" % (testdata, expectdata))raise eexcept Exception as e:logging.error("未知错误,错误信息:" + str(traceback.format_exc()))raise eelse:logging.info("搜索“%s”,期望“%s”通过" % (testdata, expectdata))def tearDown(self):self.driver.quit()if __name__ == '__main__':unittest.main()

当前目录生成的 report.log:

Fri, 22 Jan 2021 22:37:42 test.py[line:59] INFO 搜索“hello”,期望“哈喽”通过
Fri, 22 Jan 2021 22:38:07 test.py[line:59] INFO 搜索“glory”,期望“光荣”通过
Fri, 22 Jan 2021 22:38:33 test.py[line:59] INFO 搜索“morning”,期望“早晨”通过

2、ddt实战

ddt库应用在UI自动化测试中,实现编写一条测试用例的代码验证多个测试点。例如,在新浪登录页面中,存在多种测试需求,如用户名和密码输入框都为空,用户名为空、密码不为空,密码为空、用户名不为空,分别会返回不同的错误提示信息。下面通过ddt来实现以下实例,实现的代码如下: 

#!/usr/bin/env python 
#-*-coding:utf-8-*-import unittest
from selenium import webdriver 
from ddt import data,unpack,ddt@ddt
class SinaLogin(unittest.TestCase):def setUp(self):self.driver = webdriver.Firefox()self.driver.maximize_window()self.driver.get('http://mail.sina.com.cn/')self.driver.implicitly wait(30)def tearDown(self):self.driver.quit()@data((','','',u'请输入邮箱名'),('','admin',u'请输入邮箱名'),('admin','',u'您输入的邮箱名格式不正确'))@unpackdef test_login(self,username,password,result):'''验证∶测试新浪邮箱登录N种情况'''self.driver.find_element_by_id('freename').send_keys(username)self.driver.find_element_by_id('freepassword').send_keys(password)self.driver.find_element_by_link_text(u'登录').click()divText=self.driver.find_element_by_xpath('
/html/body/div[1]/div/div[2]/div/div/div/div[4]/div[1]/div[1]/div[1]/div[1]/span[1]').textself.assertEqual(divText,result)if__name__ == '__main_':unittest.main(verbosity=2)

注解:以上代码主要反映了ddt在测试用例中的应用,在@data中数据类型是元组,可以看到不同情况下的测试数据,也就是以下三个测试点:

  1. 用户名和密码为空,点击“登录”按钮,验证错误提示信息是否是“请输入邮箱名”;
  2. 用户名为空,密码不为空,点击“登录”按钮,验证错误是否是“请输入邮箱名”;
  3. 输入错误的用户名,点击“登录”按钮,验证错误是否是“您输入的邮箱格式不正确”。

在测试用例中,test_login中有三个参数,分别是username,password,result,分别与@data的元组数据一一对应。

与执行后的结果如图所示。

在图6-1-2中可以看到,三个测试用例都被执行并且通过。这就是 ddt的优秀之处,而我们写的测试代码只是一个测试用例的代码。

以上实例是把数据放在@data 中,可以把@data 中的数据分离到一个方法中,例如,存储在列表里。下面实现把@data 中的数据放在一个列表中,实现的代码如下: 

#!/usr/bin/env python 
#-*-coding:utf-8-*-import unittest
from selenium import webdriver 
from ddt import data,unpack,ddtdef getData():'''数据分离出来放到列表中'''return [[’’,’,u’请输入邮箱名’],['','admin',u'请输入邮箱名'],['admin',',u'您输入的邮箱名格式不正确']]@ddt
class SinaLogin(unittest.TestCase):def setUp(self):self.driver = webdriver.Firefox()self.driver.maximize_window()self.driver.get('http://mail.sina.com.cn/')self.driver.implicitly wait(30)def tearDown(self):self.driver.quit()@data(*getData())
@unpack
def test_login(self,username,password,result):'''验证∶测试新浪邮箱登录N种情况'''self.driver.find element by_id('freename').send_keys(username)self.driver.find element by id('freepassword').send_keys(password)self.driver.find_element_by_link_text(u'登录').click()divText=self.driver.find_element_by_xpath('
/html/body/div[1]/div/div[2]/div/div/div/div[4]/div[1]/div[1]/div[1]/div[1]/span[1]').textself.assertEqual(divText,result)if__name__ == '__main__':unittest.main(verbosity=2)

注解:在以上代码中可以看到@data 中的数据被分离到了getData 函数中,在@data中调用函数 getData时增加了“*”,是因为@data要求的数据类型是元组,加“*”后便把getData函数返回的数据列表类型变为元组类型。

3、读写json文件

数据文件:test_data_list.json

["邓肯||蒂姆","乔丹||迈克尔","库里||斯蒂芬","杜兰特||凯文","詹姆斯||勒布朗"]

测试报告模板:ReportTemplate.py

#encoding=utf-8def htmlTemplate(trData):htmlStr = u'''单元测试报告

测试报告


''' # 示例:endStr = u'''
Search WordsAssert WordsStart TimeWaste Time(s)Status
飞人乔丹21:13:2315s成功
'''# 拼接完整的测试报告HTML页面代码html = htmlStr + trData + endStrprint (html)# 生成.html文件with open(u"testTemplate.html", "w") as fp:fp.write(html)

测试程序:

from selenium import webdriver
import unittest, time
import logging, traceback
import ddt
from ReportTemplate import htmlTemplate
from selenium.common.exceptions import NoSuchElementException# 如果有no json的报错信息,请将json文件存储为utf-8 with Bom
# 初始化日志对象
logging.basicConfig(# 日志级别level = logging.INFO,# 日志格式# 时间、代码所在文件名、代码行号、日志级别名字、日志信息format = '%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s',# 打印日志的时间datefmt = '%a, %Y-%m-%d %H:%M:%S',# 日志文件存放的目录(目录必须存在)及日志文件名filename = 'report.log',# 打开日志文件的方式filemode = 'w'
)@ddt.ddt
class TestDemo(unittest.TestCase):@classmethoddef setUpClass(cls):# 整个测试过程只被调用一次TestDemo.trStr = ""def setUp(self):self.driver = webdriver.Chrome()status = None # 用于存放测试结果状态,失败'fail',成功'pass'flag = 0 # 数据驱动测试结果的标志,失败置0,成功置1@ddt.file_data("test_data_list.json")  # 读取当前目录下的测试数据,每组数据执行该方法一次def test_dataDrivenByFile(self, value):# 决定测试报告中状态单元格中内容的颜色flagDict = {0: 'red', 1: '#00AC4E'}url = "https://www.baidu.com"# 访问百度首页self.driver.get(url)# 将浏览器窗口最大化self.driver.maximize_window()print (value)# 将从.json文件中读取出的数据用“||”进行分隔成测试数据# 和期望数据testdata, expectdata = tuple(value.strip().split("||"))# 设置隐式等待时间为10秒self.driver.implicitly_wait(10)try:# 获取当前的时间戳,用于后面计算查询耗时用start = time.time()# 获取当前时间的字符串,表示测试开始时间startTime = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())# 找到搜索输入框,并输入测试数据self.driver.find_element_by_id("kw").send_keys(testdata)# 找到搜索按钮,并点击self.driver.find_element_by_id("su").click()time.sleep(3)# 断言期望结果是否出现在页面源代码中self.assertTrue(expectdata in self.driver.page_source)except NoSuchElementException as e:logging.error(u"查找的页面元素不存在,异常堆栈信息:" + str(traceback.format_exc()))status = 'fail'flag = 0except AssertionError as e:logging.info(u"搜索“%s”,期望“%s”,失败" % (testdata, expectdata))status = 'fail'flag = 0except Exception as e:logging.error(u"未知错误,错误信息:" + str(traceback.format_exc()))status = 'fail'flag = 0else:logging.info(u"搜索“%s”,期望“%s”通过" % (testdata, expectdata))status = 'pass'flag = 1# 计算耗时,从将测试数据输入到输入框中到断言期望结果之间所耗时wasteTime = time.time() - start - 3 # 减去强制等待的3秒# 每一组数据测试结束后,都将其测试结果信息插入表格行# 的HTML代码中,并将这些行HTML代码拼接到变量trStr变量中,# 等所有测试数据都被测试结束后,传入htmlTemplate()函数中# 生成完整测试报告的HTML代码TestDemo.trStr += u'''%s%s%s%.2f%s
''' % (testdata, expectdata,startTime, wasteTime, flagDict[flag], status)def tearDown(self):self.driver.quit()@classmethoddef tearDownClass(cls):# 写自定义的html测试报告# 整个测试过程只被调用一次htmlTemplate(TestDemo.trStr)if __name__ == '__main__':unittest.main()

测试日志:

Sun, 2021-01-24 20:52:30 data_drivern_by_file.py[line:80] INFO 搜索“邓肯”,期望“蒂姆”通过
Sun, 2021-01-24 20:53:01 data_drivern_by_file.py[line:80] INFO 搜索“乔丹”,期望“迈克尔”通过
Sun, 2021-01-24 20:53:32 data_drivern_by_file.py[line:80] INFO 搜索“库里”,期望“斯蒂芬”通过
Sun, 2021-01-24 20:54:04 data_drivern_by_file.py[line:80] INFO 搜索“杜兰特”,期望“凯文”通过
Sun, 2021-01-24 20:54:35 data_drivern_by_file.py[line:80] INFO 搜索“詹姆斯”,期望“勒布朗”通过

测试报告:

4、读写txt文件

数据文件:data.txt

hiphop||嘻哈
摔跤爸爸||阿米尔
超人||电影

测试程序:

from selenium import webdriver
import time
import traceback
import os# 获取当前目录下的数据文件
with open(os.path.join(os.path.dirname(os.path.abspath(__file__)), "data.txt")) as fp:data = fp.readlines()driver = webdriver.Chrome()
# 存放每组数据的测试结果
test_result = []# 读取每一行测试数据
for i in range(len(data)):try:driver.get("http://www.baidu.com")driver.find_element_by_id("kw").send_keys(data[i].split("||")[0].strip())driver.find_element_by_id("su").click()time.sleep(3)assert data[i].split('||')[1].strip() in driver.page_sourcetest_result.append(data[i].strip()+u"||成功\n")print (data[i].split('||')[0].strip()+u"搜索测试执行成功")except AssertionError as e:print (data[i].split('||')[1].strip()+u"测试断言失败")test_result.append(data[i].strip()+u"||断言失败\n")traceback.print_exc()except Exception as e:print (data[i].split('||')[1].strip()+u"测试执行失败")test_result.append(data[i].strip()+u"||异常失败\n")traceback.print_exc()# 新建txt文件存放测试结果
with open(u"result.txt","w") as fp:fp.writelines(test_result)driver.quit()

 测试结果:result.txt

hiphop||嘻哈||成功
摔跤爸爸||阿米尔||成功
超人||电影||成功

5、读写csv文件

Python 提供了对 csv 文件处理的模块,csv 文件全称为 Comma-Separated Values,csv是通用的、相对简单的文件格式,其文件以纯文件形式存储数据。

1. csv文件读取处理

在testCase包中创建test.csv文件,需要注意:创建csv为后缀的文件时,一定要先创建test.xlsx,打开文件后,再另存为test.csv,如图所示。

如果直接把 test.xlsx 修改为 test.csv,打开 csv 的文件就会出现错误提示:_csv.Error:line contains NULL byte。在test.csv中存储的文件内容,如图所示。 

下面来实现把以上csv文件里的内容读取出来,代码如下: 

#!/usr/bin/env python
#-*-coding:utf-8-*-import CSV
import osdef readCsv(row,col):rows = []with open(os.path.join(os.path.dirname(_file_),'test.csv') as f:reader = csv.reader(f)next(reader, None)for iter in reader:rows.append(iter)return ''.join(rows[row][col]).decode('gb2312')

注解:在以上代码中,首先把读取的文件内容放在rows 的列表中,''.join (rows[row][col])是把列表row转换为字符串,decode('gb2312')则把unicode编码的字符串转换为 gb2312,这样存储的中文读取出来后结果显示中文,类型是unicode。

2. csv在自动化中的实战

以新浪登录为例,实现把测试中的数据存储到csv文件中,实现的代码如下:

#!/usr/bin/env python 
#-*-coding:utf-8-*-import csv
import unittest
from selenium import webdriverdef readCsv(row,col):rows = []with open('test.csv')as f:reader = csv.reader(f)next(reader, None)for iter in reader:rows.append(iter)return ''.join(rows[row][col]).decode('gb2312')class SinaLogin(unittest.TestCase):def setUp(self):self.driver = webdriver.Firefox()self.driver.maximize_window()self.driver.get('http://mail.sina.com.cn/')self.driver.implicitly_wait(30)def tearDown(self):self.driver.quit()def login(self,username,password):self.driver.find element_by_id('freename').send_keys(username)self.driver.find element by_id('freepassword').send_keys(password)self.driver.find_element_by_link_text(u'登录').click()def divText(self):divText=self.driver.find_element_by_xpath(
'/html/body/div[1]/div/div[2]/div/div/div/div[4]/div[1]/div[1]/div[1]/div[1]/span[1]')return divText.textdef test_username_password_null(self):'''验证∶测试用户名和密码都为空的错误提示信息'''self.login(readCsv(0,0),readCsv(0,1))self.assertEqual(self.divText(),readCsv(0,2))def test sina password null(self):'''验证∶测试用户名为空密码不为空的错误提示信息'''self.login(readCsv(1,0),readCsv(1,1))self.assertTrue(self.divText(),readCsv(1,2))def test_sina_username_format(self):'''验证∶测试用户名邮箱格式不正确的错误提示信息'''self.login(readCsv(2,0),readCsv(2,1))self.assertEqual(self.divText(),readCsv(2,2))if__name__ == '__main__':unittest.main(verbosity=2)

代码执行后的结果如图所示:

6、读写excel文件

示例 1:使用 openpyxl 读取 excel 测试数据,并回写 excel 中的测试结果。

数据文件:data.xlsx

测试程序: 

from selenium import webdriver
import time
import datetime
from openpyxl import *wb = load_workbook('data.xlsx')
ws = wb.active  # 获取第一个sheet
print("最大行号:", ws.max_row)driver = webdriver.Chrome()
test_result = []# openpyxl读取excel的行号和列号是从1开始的,所以这里从2(1是标题行)开始迭代遍历测试数据
# 且使用切片,必须有结束行的索引号,不能写为[1:]
for row in ws[2: ws.max_row]:print(row[1], row[2])  # 该索引方式则从0开始try:driver.get("http://www.baidu.com")driver.find_element_by_id("kw").send_keys(row[1].value)driver.find_element_by_id("su").click()time.sleep(3)assert row[2].value in driver.page_sourcerow[3].value = time.strftime('%Y-%m-%d %H:%M:%S')row[4].value = "成功"except AssertionError as e:row[3].value = time.strftime('%Y-%m-%d %H:%M:%S')row[4].value = "断言失败"except Exception as e:row[3].value = time.strftime('%Y-%m-%d %H:%M:%S')row[4].value = "出现异常失败"driver.quit()
wb.save(u"data.xlsx")  # 注意:该方式将直接覆盖,不是更新

执行结果:data.xlsx

示例 2:使用 ddt 读取 excel 数据,并将测试结果写入日志文件 

数据文件:data.xlsx

excel 操作的工具类:ExcelUtil.py 

from openpyxl import load_workbookclass ParseExcel:def __init__(self, excelPath, sheetName):# 将要读取的excel加载到内存self.wb = load_workbook(excelPath)# 通过工作表名称获取一个工作表对象self.sheet = self.wb.get_sheet_by_name(sheetName)# 获取工作表中存在数据的区域的最大行号self.maxRowNum = self.sheet.max_rowdef getDatasFromSheet(self):# 用于存放从工作表中读取出来的数据dataList = []# 因为工作表中的第一行是标题行,所以需要去掉for line in self.sheet.rows:# 遍历工作表中数据区域的每一行,# 并将每行中各个单元格的数据取出存于列表tmpList中,# 然后再将存放一行数据的列表添加到最终数据列表dataList中tmpList = []tmpList.append(line[1].value)tmpList.append(line[2].value)dataList.append(tmpList)# 将获取工作表中的所有数据的迭代对象返回return dataList[1:]if __name__ == '__main__':excelPath = 'data.xlsx'sheetName = "搜索数据表"pe = ParseExcel(excelPath, sheetName)print (pe.getDatasFromSheet())for i in pe.getDatasFromSheet():print (i[0], i[1])

执行结果:

[['邓肯', '蒂姆111'], ['乔丹', '迈克尔'], ['库里', '斯蒂芬']]
邓肯 蒂姆111
乔丹 迈克尔
库里 斯蒂芬

测试程序:

# encoding=utf-8
from selenium import webdriver
import unittest, time
import logging, traceback
import ddt
from ExcelUtil import ParseExcel
from selenium.common.exceptions import NoSuchElementException# 初始化日志对象
logging.basicConfig(# 日志级别level = logging.INFO,# 日志格式# 时间、代码所在文件名、代码行号、日志级别名字、日志信息format = '%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s',# 打印日志的时间datefmt = '%a, %Y-%m-%d %H:%M:%S',# 日志文件存放的目录(目录必须存在)及日志文件名filename = 'dataDriveRreport.log',# 打开日志文件的方式filemode = 'w'
)excelPath = 'data.xlsx'
sheetName = "搜索数据表"# 创建ParseExcel类的实例对象
excel = ParseExcel(excelPath, sheetName)@ddt.ddt
class TestDemo(unittest.TestCase):def setUp(self):self.driver = webdriver.Chrome()@ddt.data(*excel.getDatasFromSheet())  # 解包二维列表中的每组子列表数据def test_dataDrivenByFile(self, data):print("*****", data)testData, expectData = tuple(data)url = "http://www.baidu.com"# 访问百度首页self.driver.get(url)# 将浏览器窗口最大化self.driver.maximize_window()print (testData, expectData)# 设置隐式等待时间为10秒self.driver.implicitly_wait(10)try:# 获取当前的时间戳,用于后面计算查询耗时用start = time.time()# 获取当前时间的字符串,表示测试开始时间startTime = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())# 找到搜索输入框,并输入测试数据self.driver.find_element_by_id("kw").send_keys(testData)# 找到搜索按钮,并点击self.driver.find_element_by_id("s").click()time.sleep(3)# 断言期望结果是否出现在页面源代码中self.assertTrue(expectData in self.driver.page_source)end=time.time()print("搜索%s,期望%s" %(testData, expectData))except NoSuchElementException as e:logging.error("查找的页面元素不存在,异常堆栈信息:" + str(traceback.format_exc()))except AssertionError as e:print("断言失败了")logging.info("搜索“%s”,期望“%s”,失败" % (testData, expectData))except Exception as e:logging.error("未知错误,错误信息:" + str(traceback.format_exc()))else:logging.info("搜索“%s”,期望“%s”通过,耗时%s秒" % (testData, expectData, (end-start)/1000))def tearDown(self):self.driver.quit()if __name__ == '__main__':unittest.main()

执行结果:dataDriveRreport.log

Fri, 2021-01-22 23:56:42 data_drivern_by_excel.py[line:67] INFO 搜索“邓肯”,期望“蒂姆111”,失败
Fri, 2021-01-22 23:57:15 data_drivern_by_excel.py[line:71] INFO 搜索“乔丹”,期望“迈克尔”通过,耗时0.004345748424530029秒
Fri, 2021-01-22 23:57:48 data_drivern_by_excel.py[line:71] INFO 搜索“库里”,期望“斯蒂芬”通过,耗时0.004197439908981323秒

7、读写xml文件

Xml 文件是可扩展标记语言。在自动化测试中,也可以把数据存储到 Xml文件中,这样应用时可直接从Xml文件中读取。

1. Xml文件的读取

使用标准库 xml.dom.minidom,通过 document的方式读取 Xml文件的内容,在这里创建sina.xml文件,文件的内容为:


http://mail.sina.com.cn/

下面读取xml文件中url节点中的内容和errorMsg节点中子节点的内容,实现的代码如下:

#!/usr/bin/env python
#-*-coding:utf-8-*-import xml.dom.minidomdef getxmlData(value):'''获取xml单节点中的数据∶param value∶xml文件中单节点的名称'''dom = xml.dom.minidom.parse('sina.xml')db = dom.documentElementname = db.getElementsByTagName(value)nameValue = name[0]return nameValue.firstChild.datadef getXmlUser(parent, child):'''获取xml子节点中的数据∶param parent∶xml文件中父节点的名称∶param child∶xml文件中子节点的名称'''dom=xml.dom.minidom.parse('sina.xml')db = dom.documentElementitemlist = db.getElementsByTagName(parent)item = itemlist[0]return item.getAttribute(child)

注解:在以上代码中,对 Xml 文件的处理是通过 document 的形式来读取Xml 文件中的内容,dom.documentElement 获得文档元素的结构来完成的。方法getXmlUser 用来获取节点里面的数据,而方法 getXmlData 则获取单节点的数据。

2. Xml在自动化测试中的实战

这里依然以新浪邮箱登录为例,测试当登录邮箱名为空和登录时填写的邮箱格式不正确时,返回的错误提示信息,并把测试中用到的数据分离到 Xml 文件中,实现的代码如下:

#!/usr/bin/env python 
#-*-coding:utf-8-*-import xml.dom.minidcm 
import unittest
from selenium import webdriver 
import time as tdef getXmlData(value):'''获取xm2单节点中的数据∶param value∶xml文件中单节点的名称'''dom=xml.dom.minidom.parse('sina.xml')db = dom.documentElementname = db.getElementsByIagName(value)nameValue = name[0]return nameValue,firstChild.datadef getXmlUser(parent,child):'''    获取xmi了节点中的数据∶param pazent∶xml文件中父节点的名称∶param child∶xml文件中子节点的名称'''dom=xml.dom.minidom.parse('sina.xml')db = dom.documentElementitemlist = db.getElementsByTagName(parent)item = itemlist[0]return item.getAttribute(child)class SinaTest(unittest.TestCase):def setUp(self):self.driver=webdriver.Firefox()self.driver.maximize_window()seif.driver.get(getXmlData('url')self.driver.implicitly_wait(30)def tearDown(self):self.criver.quit()def login(self,username,password):t.s_eep(2)#邮箱账号输入框self.driver.find element by id('freename').send_keys(userrame)t.s_eep(2)self.driver.find_element_by_id('freepassword').send keys(password)t.s_eep(2)self.criver.find_element_by_link_text('登录').click()@propertydef getLoginError(self):'''这回点击"登录"按钮后的错误提示信息'''t.s_eep(2)log_nError=se_f.driver.find_element_by_xpath('
/html/body/div[1]/div/div(2]/div/div/div[4]/div[1]/div[1]/div[1]/div[1]/span[1]')return loginError.textdef test_sina_login emailNull(self):'''新浪邮箱普录∶登录账号邮箱为空验证'''self.login('','')self.assertEqual(self.getLoginError,getXmlUser('errorMsg','emailNull'))def test sina login_emailFormat(self):'''新浪邮箱登录∶登录账号邮箱格式填写错误验证'''self.login('wuyal303','adminasd')self.assertEqual(self.getLoginError, getXmlUser('errorMsg','emailFormat'))if__name__ == '__main__':unittest.main(verbosity=2)

注解:在以上测试代码中,把数据分离到xml文件后,则登录的测试用例中不管是邮箱为空还是邮箱格式错误,直接从xml文件中读取数据与实际测试获取的数据进行断言比较。数据分离到xml后,即使错误提示信息进行了优化也可以直接在xml文件中修改。

8、读写mysql数据库

数据表等创建语句:Sql.py

# 创建test数据库sql语句
create_database = 'CREATE DATABASE IF NOT EXISTS test DEFAULT CHARSET utf8 COLLATE utf8_general_ci;'# 创建testdata表
drop_table_if_exist_sql="drop table if exists testdata;"create_table = """create table testdata(id int not null auto_increment comment '主键',bookname varchar(40) unique not null comment '书名',author varchar(30) not null comment '作者',test_result varchar(30) default null,primary key(id))engine=innodb character set utf8 comment '测试数据表';
"""

初始化数据操作:DatabaseInit.py

import pymysql
from Sql import *# 本类用于完成初始化数据操作
# 创建数据库,创建数据表,向表中插入测试数据
class DataBaseInit(object):def __init__(self, host, port, dbName, username, password, charset):self.host = hostself.port = portself.db = dbNameself.user = usernameself.passwd = passwordself.charset = charsetdef create(self):try:# 连接mysql数据库conn = pymysql.connect(host = self.host,port = self.port,user = self.user,passwd = self.passwd,charset = self.charset)# 获取数据库游标cur = conn.cursor()# 创建数据库cur.execute(create_database)# 选择创建好的gloryroad数据库conn.select_db("gloryroad")# 创建测试表cur.execute(drop_table_if_exist_sql)cur.execute(create_table)except pymysql.Error as e:raise eelse:# 关闭游标cur.close()# 提交操作conn.commit()# 关闭连接conn.close()print (u"创建数据库及表成功")def insertDatas(self):try:# 连接mysql数据库中具体某个库conn = pymysql.connect(host = self.host,port = self.port,db = self.db,user = self.user,passwd = self.passwd,charset = self.charset)cur = conn.cursor()# 向测试表中插入测试数据sql = "insert into testdata(bookname, author) values(%s, %s);"res = cur.executemany(sql, [('Selenium WebDriver实战宝典', '吴晓华'),('HTTP权威指南', '古尔利'),('探索式软件测试', '惠特克'),('暗时间', '刘未鹏')])except pymysql.Error as e:raise eelse:conn.commit()print (u"初始数据插入成功")# 确认插入数据成功cur.execute("select * from testdata;")for i in cur.fetchall():print (i[1], i[2])cur.close()conn.close()if __name__ == '__main__':db = DataBaseInit(host="localhost",port=3306,dbName="xxx",username="xxx",password="xxx",charset="utf8")db.create()db.insertDatas()print ("数据库初始化结束")

mysql 操作工具类:MysqlUtil.py

import pymysql
from DatabaseInit import DataBaseInitclass MyMySQL(object):def __init__(self, host, port, dbName, username, password, charset):# 进行数据库初始化dbInit = DataBaseInit(host, port, dbName, username, password, charset)dbInit.create()dbInit.insertDatas()self.conn = pymysql.connect(host = host,port = port,db = dbName,user = username,passwd = password,charset = charset)self.cur = self.conn.cursor()def getDataFromDataBases(self):# 从testdata表中获取需要的测试数据# bookname作为搜索关键词,author作为预期关键词self.cur.execute("select bookname, author from testdata;")# 从查询区域取回所有查询结果datasTuple = self.cur.fetchall()return datasTupledef closeDatabase(self):# 数据库后期清理工作self.cur.close()self.conn.commit()self.conn.close()if __name__ == '__main__':db = MyMySQL(host = "localhost",port = 3306,dbName = "xxx",username = "xxx",password = "xxx",charset = "utf8")print (db.getDataFromDataBases())

测试程序:

from selenium import webdriver
import time
import pymysqldef get_test_data():conn = pymysql.connect(host = "127.0.0.1",port = 3306,user = "xxx",passwd = "xxx" ,db = "xxx",charset = "utf8")# 使用cursor()方法获取数据库的操作游标cursor = conn.cursor()cursor.execute("select * from testdata;")resSet = cursor.fetchall()print("共%s条数据。" % len(resSet))print(resSet)# 关闭游标cursor.close()# 提交事务conn.commit()# 关闭数据库连接conn.close()return resSetdef update_test_result(data,result):conn = pymysql.connect(host = "127.0.0.1",port = 3306,user = "root",passwd = "gloryroad" ,db = "gloryroad",charset = "utf8")# 使用cursor()方法获取数据库的操作游标cursor = conn.cursor()print('update testdata set test_result="'+result+'" where bookname="'+data+'";')update=cursor.execute('update testdata set test_result="'+result+'" where bookname="'+data+'";')print( u"修改语句受影响的行数:", update)# 关闭游标cursor.close()# 提交事务conn.commit()# 关闭数据库连接conn.close()driver=webdriver.Ie(executable_path="e:\\IEDriverServer")
test_result=[]for data in get_test_data():print("-------------", data)try:driver.get("http://www.baidu.com")driver.find_element_by_id("kw").send_keys(data[1])driver.find_element_by_id("su").click()time.sleep(3)assert data[2] in driver.page_sourceupdate_test_result(data[1], "成功")except AssertionError as e:print( data[2] + "断言失败")update_test_result(data[1], "断言失败")except Exception as e:print(e)print(data[1] + "测试执行出现异常")update_test_result(data[1], "执行出现异常")driver.quit()

四、关键字驱动

1、关键字驱动框架简介

关键字驱动测试是数据驱动测试的一种改进类型,它也被称为表格驱动测试或者基于动作字的测试。

主要关键字包括三类:被操作对象(Item)、操作行为(Operation)和操作值(Value),用面向对象形式可将其表现为 Item.Operation(Value)。

将测试逻辑按照这些关键字进行分解,形成数据文件。用关键字的形式将测试逻辑封装在数据文件中,测试工具只要能够解释这些关键字即可对其应用自动化。

优势:

  1. 执行人员可以不需要太多的技术:一旦框架建立,手工测试人员和非技术人员都可以很容易的编写自动化测试脚本。
  2. 简单易懂:它存在Excel表格中,没有编码,测试脚本容易阅读和理解。关键字和操作行为这样的手工测试用例,使它变得更容易编写和维护。
  3. 早期介入:可以在应用未提交测试之前,就可以建立关键字驱动测试用例对象库,从而减少后期工作。使用需求和其它相关文档进行收集信息,关键字数据表可以建立手工测试程序。
  4. 代码的重用性:用关键字的形式将测试用例及数据进行组装并解释执行,提高代码的可重用性。

2、工程结构说明

整个测试框架分为四层,通过分层的方式,测试代码更容易理解,维护起来较为方便。

第一层是“测试工具层”:

  • util 包:用于实现测试过程中调用的工具类方法,例如读取配置文件、页面元素的操作方法、操作 Excel 文件、生成测试报告、发送邮件等。
  • conf 包:配置文件及全局变量。
  • log 目录:日志输出文件。
  • exception_pic 目录:失败用例的截图保存目录。

第二层是“服务层”:相当于对测试对象的一个业务封装。对于接口测试,是对远程方法的一个实现;对于 UI 测试,是对页面元素或操作的一个封装

  • action 包:封装具体的页面动作,如点击、输入文本等。

第三层是“测试用例逻辑层”:该层主要是将服务层封装好的各个业务对象,组织成测试逻辑,进行校验

  • bussiness_process 包:基于关键字的形式,实现单条、多条用例的测试脚本逻辑。
  • test_data 目录:Excel 数据文件,包含用例步骤、被操作对象、操作动作、操作值、测试结果等。

第四层是“测试场景层”:将测试用例组织成测试场景,实现各种级别 cases 的管理,如冒烟,回归等测试场景

  • main.py:本框架工程的运行主入口。

 

框架特点:

  1. 基于关键字测试框架,即使不懂开发技术的测试人员也可以实施自动化测试,便于在整个测试团队中推广和使用自动化测试技术,降低自动化测试实施的技术门槛。
  2. 使用外部测试数据文件,使用Excel管理测试用例的集合和每个测试用例的所有执行步骤,实现在一个文件中完成测试用例的维护工作。
  3. 通过定义关键字、操作元素的定位方式和定位表达式和操作值,就可以实现每个测试步骤的执行,可以更加灵活地实现自动化测试的需求。
  4. 基于关键字的方式,可以进行任意关键字的扩展,以满足更加复杂的自动化测试需求。
  5. 实现定位表达式和测试代码的分离,实现定位表达式直接在数据文件中进行维护。
  6. 框架提供日志功能,方便调试和监控自动化测试程序的执行。

3、工程代码实现

1. action 包

action 包为框架第二层“服务层”,相当于对测试对象的一个业务封装。对于接口测试,是对远程方法的一个实现;对于 UI 测试,是对页面元素或操作的一个封装。

page_action.py:

该模块基于关键字格式,封装了页面操作的常用函数,如打开浏览器、点击、输入文本等。

from selenium import webdriver
import time
import traceback
from util.datetime_util import *
from util.find_element_util import *
from util.ini_parser import *
from util.log_util import *DRIVER = ""# 初始化浏览器
def init_browser(browser_name):global DRIVERif browser_name.lower() == "chrome":DRIVER = webdriver.Chrome(CHROME_DRIVER)elif browser_name.lower() == "firefox":DRIVER = webdriver.Firefox(FIREFOX_DRIVER)elif browser_name.lower() == "ie":DRIVER = webdriver.Ie(IE_DRIVER)else:warning("浏览器【%s】不支持,已默认启动chrome" % browser_name)DRIVER = webdriver.Chrome(CHROME_DRIVER)# 访问指定url
def visit(url):global DRIVERDRIVER.get(url)# 输入操作
def input(locate_method, locate_exp, value):global DRIVER# 方式1:直接传定位方式和定位表达式if locate_method in ["id", "xpath", "classname", "name", "tagname", "linktext","partial link text", "css selector"]:find_element(DRIVER, locate_method, locate_exp).send_keys(value)# 方式2:通过ini文件的key找到value,再分割定位方式和定位表达式else:parser = IniParser(ELEMENT_FILE_PATH)locate_method, locate_exp = tuple(parser.get_value(locate_method, locate_exp).split(">"))find_element(DRIVER, locate_method, locate_exp).send_keys(value)# 点击操作
def click(locate_method, locate_exp):global DRIVER# 方式1:直接传定位方式和定位表达式if locate_method in ["id", "xpath", "classname", "name", "tagname", "linktext","partial link text", "css selector"]:find_element(DRIVER, locate_method, locate_exp).click()# 方式2:通过ini文件的key找到value,再分割定位方式和定位表达式else:parser = IniParser(ELEMENT_FILE_PATH)locate_method, locate_exp = tuple(parser.get_value(locate_method, locate_exp).split(">"))find_element(DRIVER, locate_method, locate_exp).click()# 清空输入框操作
def clear(locate_method, locate_exp):global DRIVER# 方式1:直接传定位方式和定位表达式if locate_method in ["id", "xpath", "classname", "name", "tagname", "linktext","partial link text", "css selector"]:find_element(DRIVER, locate_method, locate_exp).clear()# 方式2:通过ini文件的key找到value,再分割定位方式和定位表达式else:parser = IniParser(ELEMENT_FILE_PATH)locate_method, locate_exp = tuple(parser.get_value(locate_method, locate_exp).split(">"))find_element(DRIVER, locate_method, locate_exp).clear()# 切换frame
def switch_frame(locate_method, locate_exp):global DRIVER# 方式1:直接传定位方式和定位表达式if locate_method in ["id", "xpath", "classname", "name", "tagname", "linktext","partial link text", "css selector"]:DRIVER.switch_to.frame(find_element(DRIVER, locate_method, locate_exp))# 方式2:通过ini文件的key找到value,再分割定位方式和定位表达式else:parser = IniParser(ELEMENT_FILE_PATH)locate_method, locate_exp = tuple(parser.get_value(locate_method, locate_exp).split(">"))DRIVER.switch_to.frame(find_element(DRIVER, locate_method, locate_exp))# 切换主frame
def switch_home_frame():global DRIVERDRIVER.switch_to.default_content()# 断言
def assert_word(keyword):global DRIVERassert keyword in DRIVER.page_source# 休眠
def sleep(times):time.sleep(int(times))# 关闭浏览器
def quit():global DRIVERDRIVER.quit()# 截图函数
def take_screenshot():global DRIVER# 创建当前日期目录dir = os.path.join(SCREENSHOT_PATH, get_chinese_date())if not os.path.exists(dir):os.makedirs(dir)# 以当前时间为文件名file_name = get_chinese_time()file_path = os.path.join(dir, file_name+".png")try:DRIVER.get_screenshot_as_file(file_path)# 返回截图文件的绝对路径return file_pathexcept:error("截图发生异常【{}】\n{}".format(file_path, traceback.format_exc()))return file_pathif __name__ == "__main__":init_browser("chrome")visit("http://mail.126.com")print(take_screenshot())

2. business_process 包

business_process 包是框架第三层“测试用例逻辑层”,该层主要是将服务层封装好的各个业务对象,组织成测试逻辑,进行校验。

case_process.py:

  • 测试用例文件的一行数据,拼接其中的操作动作、操作对象、操作值等关键字,形成与 page_action.py 中的函数相对应的字符串,并通过 eval() 转成表达式以执行用例。
  • 记录该用例的测试结果,如测试执行结果、测试执行时间等。
  • 如需数据驱动的用例集,则获取数据驱动的数据源集合,循环将每组数据传递给用例步骤。
  • 如果遇到需要参数化的值 ${变量名},则根据数据驱动的数据源,根据变量名进行参数化。
import traceback
import re
from util.global_var import *
from util.log_util import *
from util.datetime_util import *
from util.excel_util import Excel
from action.page_action import *# 执行一条测试用例(即一行测试数据)
def execute_case(excel_file_path, case_data, test_data_source=None):# 用例数据格式校验if not isinstance(case_data, (list, tuple)):error("测试用例数据格式有误!测试数据应为列表或元组类型!【%s】" % case_data)case_data[TEST_SCRIPT_EXCEPTION_INFO_COL] = "测试用例数据格式有误!应为列表或元组类型!【%s】" % case_datacase_data[TEST_SCRIPT_TEST_RESULT_COL] = "Fail"# 该用例无需执行if case_data[TEST_SCRIPT_IS_EXECUTE_COL].lower() == "n":info("测试用例步骤【%s】无需执行" % case_data[TEST_SCRIPT_NAME_COL])return# excel对象初始化if isinstance(excel_file_path, Excel):excel = excel_file_path  # 如果传入的是excel对象,则直接使用else:excel = Excel(excel_file_path)  # 如果传入的是文件路径,则初始化excel对象# 获取各关键字operation_action = case_data[TEST_SCRIPT_ACTION_COL]  # 操作动作(即函数名)locate_method = case_data[TEST_SCRIPT_LOCATE_METHOD_COL]  # 定位方式locate_expression = case_data[TEST_SCRIPT_LOCATE_EXPRESSION_COL]  # 定位表达式operation_value = case_data[TEST_SCRIPT_VALUE_COL]  # 操作值# 由于数据驱动,需要进行参数化的值if test_data_source:if re.search(r"\$\{\w+\}", str(operation_value)):# 取出需要参数化的值key = re.search(r"\$\{(\w+)\}", str(operation_value)).group(1)operation_value = re.sub(r"\$\{\w+\}", str(test_data_source[key]), str(operation_value))# 将参数化后的值回写excel测试结果中,便于回溯case_data[TEST_SCRIPT_VALUE_COL] = operation_value# 拼接关键字函数if locate_method and locate_expression:if operation_value:func = "%s('%s', '%s', '%s')" % (operation_action, locate_method, locate_expression, operation_value)else:func = "%s('%s', '%s')" % (operation_action, locate_method, locate_expression)else:if operation_value:func = "%s('%s')" % (operation_action, operation_value)else:func = "%s()" % operation_action# 执行用例try:eval(func)info("测试用例步骤执行成功:【{}】 {}".format(case_data[TEST_SCRIPT_NAME_COL], func))case_data[TEST_SCRIPT_TEST_RESULT_COL] = "Pass"except:info("测试用例步骤执行失败:【{}】 {}".format(case_data[TEST_SCRIPT_NAME_COL], func))case_data[TEST_SCRIPT_TEST_RESULT_COL] = "Fail"error(traceback.format_exc())# 进行截图case_data[TEST_SCRIPT_SCREENSHOT_PATH_COL] = take_screenshot()# 异常信息记录case_data[TEST_SCRIPT_EXCEPTION_INFO_COL] = traceback.format_exc()# 测试时间记录case_data[TEST_SCRIPT_TEST_TIME_COL] = get_english_datetime()return case_dataif __name__ == "__main__":excel = Excel(TEST_DATA_FILE_PATH)excel.get_sheet("登录(调试用)")all_data = excel.get_all_row_data()for data in all_data[1:]:execute_case(excel, data)

data_source_process.py:

本模块实现了获取数据驱动所需的数据源集合。

  • 根据数据源 sheet 名,获取该 sheet 所有行数据,每行数据作为一组测试数据。
  • 每行数据作为一个字典,存储在一个列表中。如 [{"登录用户名": "xxx", "登录密码": "xxx", ...}, {...}, ...]
from util.excel_util import Excel
from util.global_var import *
from util.log_util import *# 数据驱动
# 每行数据作为一个字典,存储在一个列表中。如[{"登录用户名": "xxx", "登录密码": "xxx", ...}, {...}, ...]
def get_test_data(excel_file_path, sheet_name):# excel对象初始化if isinstance(excel_file_path, Excel):excel = excel_file_pathelse:excel = Excel(excel_file_path)# 校验sheet名if not excel.get_sheet(sheet_name):error("sheet【】不存在,停止执行!" % sheet_name)returnresult_list = []all_row_data = excel.get_all_row_data()if len(all_row_data) <= 1:error("sheet【】数据不大于1行,停止执行!" % sheet_name)return# 将参数化的测试数据存入全局字典head_line_data = all_row_data[0]for data in all_row_data[1:]:if data[-1].lower() == "n":continuerow_dict = {}# 最后一列为“是否执行”列,无需取值for i in range(len(data[:-1])):row_dict[head_line_data[i]] = data[i]result_list.append(row_dict)return result_listif __name__ == "__main__":from util.global_var import *print(get_test_data(TEST_DATA_FILE_PATH, "搜索词"))# [{'搜索词': 'python', '断言词': 'python'}, {'搜索词': 'mysql', '断言词': 'mysql5.6'}]

main_process.py:

本模块基于 case_process.py 和 data_source_process.py,实现关键字驱动+数据驱动的测试用例集的执行。

  • suite_process():执行具体的测试用例步骤 sheet(如“登录”sheet、“添加联系人”sheet 等)
  • main_suite_process():执行“测试用例”主 sheet 的用例集。每行用例集对应一个用例步骤 sheet 和数据源 sheet。
from util.excel_util import *
from util.datetime_util import *
from util.log_util import *
from util.global_var import *
from business_process.case_process import execute_case
from business_process.data_source_process import get_test_data# 执行具体模块的用例sheet(登录sheet,添加联系人sheet等)
def suite_process(excel_file_path, sheet_name, test_data_source=None):""":param excel_file_path: excel文件绝对路径或excel对象:param sheet_name: 测试步骤sheet名:param test_data_source: 数据驱动的数据源,默认没有:return:"""# 记录测试结果统计global TOTAL_CASEglobal PASS_CASEglobal FAIL_CASE# 整个用例sheet的测试结果,默认为全部通过suite_test_result = True# excel对象初始化if isinstance(excel_file_path, Excel):excel = excel_file_pathelse:excel = Excel(excel_file_path)if not excel.get_sheet(sheet_name):error("sheet【%s】不存在,停止执行!" % sheet_name)return# 获取测试用例集sheet的全部行数据all_row_data = excel.get_all_row_data()if len(all_row_data) <= 1:error("sheet【%s】数据不大于1行,停止执行!" % sheet_name)return# 标题行数据head_line_data = all_row_data[0]# 切换到测试结果明细sheet,准备写入测试结果if not excel.get_sheet("测试结果明细"):error("【测试结果明细】sheet不存在,停止执行!")returnexcel.write_row_data(head_line_data, None, True, "green")# 执行每行的测试用例for row_data in all_row_data[1:]:result_data = execute_case(excel, row_data, test_data_source)# 无需执行的测试步骤,跳过if result_data is None:continueTOTAL_CASE += 1if result_data[TEST_SCRIPT_TEST_RESULT_COL].lower() == "fail":suite_test_result = FalseFAIL_CASE += 1else:PASS_CASE += 1excel.write_row_data(result_data)# 切换到测试结果统计sheet,写入统计数据if not excel.get_sheet("测试结果统计"):error("【测试结果统计】sheet不存在,停止执行!")returnexcel.insert_row_data(1, [TOTAL_CASE, PASS_CASE, FAIL_CASE])return excel, suite_test_result# 执行【测试用例集】主sheet的用例集
def main_suite_process(excel_file_path, sheet_name):# 初始化excel对象excel = Excel(excel_file_path)if not excel:error("excel数据文件【%s】不存在!" % excel_file_path)returnif not excel.get_sheet(sheet_name):error("sheet名称【%s】不存在!" % sheet_name)return# 获取所有行数据all_row_datas = excel.get_all_row_data()if len(all_row_datas) <= 1:error("sheet【%s】数据不大于1行,停止执行!" % sheet_name)return# 标题行数据head_line_data = all_row_datas[0]for row_data in all_row_datas[1:]:# 校验用例步骤sheet名是否存在if row_data[MAIN_CASE_SCRIPT_SHEET_COL] not in excel.get_all_sheet():error("#" * 50 + " 用例步骤集【%s】不存在! " % row_data[MAIN_CASE_SCRIPT_SHEET_COL] + "#" * 50 + "\n")row_data[MAIN_CASE_TEST_RESULT_COL] = "Fail"excel.write_row_data(head_line_data, None, True, "red")excel.write_row_data(row_data)continue# 跳过不需要执行的测试用例集if row_data[MAIN_CASE_IS_EXECUTE_COL].lower() == "n":info("#" * 50 + " 测试用例集【%s】无需执行!" % row_data[MAIN_CASE_CASE_NAME_COL] + "#" * 50 + "\n")continue# 记录本用例集的测试时间row_data[MAIN_CASE_TEST_TIME_COL] = get_english_datetime()# 判断本测试用例集是否进行数据驱动if row_data[MAIN_CASE_DATA_SOURCE_SHEET_COL]:# 校验测试数据集sheet名是否存在if row_data[MAIN_CASE_DATA_SOURCE_SHEET_COL] not in excel.get_all_sheet():error("#" * 50 + " 测试数据集【%s】不存在! " % row_data[MAIN_CASE_DATA_SOURCE_SHEET_COL] + "#" * 50 + "\n")row_data[MAIN_CASE_TEST_RESULT_COL] = "Fail"excel.write_row_data(head_line_data, None, True, "red")excel.write_row_data(row_data)continue# 获取测试数据集test_data_source = get_test_data(excel, row_data[MAIN_CASE_DATA_SOURCE_SHEET_COL])# 每条数据进行一次本用例集的测试for data_source in test_data_source:info("-" * 50 + " 测试用例集【%s】开始执行!" % row_data[MAIN_CASE_CASE_NAME_COL] + "-" * 50)excel, test_result_flag = suite_process(excel, row_data[MAIN_CASE_SCRIPT_SHEET_COL], data_source)# 记录本用例集的测试结果if test_result_flag:info("#" * 50 + " 测试用例集【%s】执行成功! " % row_data[MAIN_CASE_CASE_NAME_COL] + "#" * 50 + "\n")row_data[MAIN_CASE_TEST_RESULT_COL] = "Pass"else:error("#" * 50 + " 测试用例集【%s】执行失败! " % row_data[MAIN_CASE_CASE_NAME_COL] + "#" * 50 + "\n")row_data[MAIN_CASE_TEST_RESULT_COL] = "Fail"# 全部测试步骤结果写入后,最后写入本用例集的标题行和测试结果行数据# 切换到“测试结果明细”sheet,以写入测试执行结果excel.get_sheet("测试结果明细")excel.write_row_data(head_line_data, None, True, "red")excel.write_row_data(row_data)# 本用例集无需数据驱动else:info("-" * 50 + " 测试用例集【%s】开始执行!" % row_data[MAIN_CASE_CASE_NAME_COL] + "-" * 50)excel, test_result_flag = suite_process(excel, row_data[MAIN_CASE_SCRIPT_SHEET_COL])# 记录本用例集的测试结果if test_result_flag:info("#" * 50 + " 测试用例集【%s】执行成功! " % row_data[MAIN_CASE_SCRIPT_SHEET_COL] + "#" * 50 + "\n")row_data[MAIN_CASE_TEST_RESULT_COL] = "Pass"else:error("#" * 50 + " 测试用例集【%s】执行失败! " % row_data[MAIN_CASE_SCRIPT_SHEET_COL] + "#" * 50 + "\n")row_data[MAIN_CASE_TEST_RESULT_COL] = "Fail"# 全部测试步骤结果写入后,最后写入本用例集的标题行和测试结果行数据# 切换到“测试结果明细”sheet,以写入测试执行结果excel.get_sheet("测试结果明细")excel.write_row_data(head_line_data, None, True, "red")excel.write_row_data(row_data)return excelif __name__ == "__main__":from util.report_util import create_excel_report_and_send_email# excel, _ = suite_process(TEST_DATA_FILE_PATH_1, "登录1")excel = main_suite_process(TEST_DATA_FILE_PATH, "测试用例集")create_excel_report_and_send_email(excel, "182230124@qq.com", "UI自动化测试", "请查收附件:UI自动化测试报告")

3. util 包

util 包属于第一层的测试工具层:用于实现测试过程中调用的工具类方法,例如读取配置文件、页面元素的操作方法、操作 Excel 文件、生成测试报告、发送邮件等。

global_var.py:

本模块用于定义测试过程中所需的全局变量。

import os# 工程根路径
PROJECT_ROOT_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))# 元素定位方法的ini配置文件路径
ELEMENT_FILE_PATH = os.path.join(PROJECT_ROOT_PATH, "conf", "ElementsRepository.ini")# excel文件路径
TEST_DATA_FILE_PATH = os.path.join(PROJECT_ROOT_PATH, "test_data", "test_case.xlsx")# 驱动路径
CHROME_DRIVER = "E:\\auto_test_driver\\chromedriver.exe"
IE_DRIVER = "E:\\auto_test_driver\\IEDriverServer.exe"
FIREFOX_DRIVER = "E:\\auto_test_driver\\geckodriver.exe"# 截图路径
SCREENSHOT_PATH = os.path.join(PROJECT_ROOT_PATH, "exception_pic")# 日志配置文件路径
LOG_CONF_FILE_PATH = os.path.join(PROJECT_ROOT_PATH, "conf", "Logger.conf")# 测试报告存放路径
TEST_REPORT_FILE_DIR = os.path.join(PROJECT_ROOT_PATH, "test_report")# 对应excel测试数据文件中具体模块sheet中的列号
TEST_SCRIPT_NAME_COL = 1
TEST_SCRIPT_ACTION_COL = 2
TEST_SCRIPT_LOCATE_METHOD_COL = 3
TEST_SCRIPT_LOCATE_EXPRESSION_COL = 4
TEST_SCRIPT_VALUE_COL = 5
TEST_SCRIPT_IS_EXECUTE_COL = 6
TEST_SCRIPT_TEST_TIME_COL = 7
TEST_SCRIPT_TEST_RESULT_COL = 8
TEST_SCRIPT_EXCEPTION_INFO_COL = 9
TEST_SCRIPT_SCREENSHOT_PATH_COL = 10# 对应excel测试数据文件中“测试用例集”sheet列号
MAIN_CASE_CASE_NAME_COL = 3
MAIN_CASE_BROWSER_NAME_COL = 5
MAIN_CASE_SCRIPT_SHEET_COL = 6
MAIN_CASE_DATA_SOURCE_SHEET_COL = 7
MAIN_CASE_IS_EXECUTE_COL = 8
MAIN_CASE_TEST_TIME_COL = 9
MAIN_CASE_TEST_RESULT_COL = 10# 测试结果统计
TOTAL_CASE = 0
PASS_CASE = 0
FAIL_CASE = 0if __name__ == "__main__":print(PROJECT_ROOT_PATH)

find_element_util.py:

本模块封装了基于显式等待的界面元素定位方法。

from selenium.webdriver.support.ui import WebDriverWait# 显式等待一个元素
def find_element(driver, locate_method, locate_exp):# 显式等待对象(最多等10秒,每0.2秒判断一次等待的条件)return WebDriverWait(driver, 10, 0.2).until(lambda x: x.find_element(locate_method, locate_exp))# 显式等待一组元素
def find_elements(driver, locate_method, locate_exp):# 显式等待对象(最多等10秒,每0.2秒判断一次等待的条件)return WebDriverWait(driver, 10, 0.2).until(lambda x: x.find_elements(locate_method, locate_exp))

excel_util.py:

本模块封装了对 excel 的读写操作(openpyxl 版本:3.0.4)。

import os
from openpyxl import load_workbook
from openpyxl.styles import PatternFill, Font, Side, Border
from util.datetime_util import *
from util.global_var import *
from util.log_util import *# 支持excel读写操作的工具类
class Excel:# 初始化读取excel文件def __init__(self, file_path):if not os.path.exists(file_path):returnself.wb = load_workbook(file_path)# 初始化默认sheetself.ws = self.wb.activeself.data_file_path = file_path# 初始化颜色字典,供设置样式用self.color_dict = {"red": "FFFF3030", "green": "FF008B00"}def get_all_sheet(self):return self.wb.get_sheet_names()# 打开指定sheetdef get_sheet(self, sheet_name):if sheet_name not in self.get_all_sheet():error("sheet名称【%s】不存在!" % sheet_name)returnself.ws = self.wb.get_sheet_by_name(sheet_name)return True# 获取最大行号def get_max_row_no(self):# openpyxl的API的行、列索引默认都从1开始return self.ws.max_row# 获取最大列号def get_max_col_no(self):return self.ws.max_column# 获取所有行数据def get_all_row_data(self, head_line=True):# 是否需要标题行数据的标识,默认需要if head_line:min_row = 1  # 行号从1开始,即1为标题行else:min_row = 2result = []# min_row=None:默认获取标题行数据for row in self.ws.iter_rows(min_row=min_row, max_row=self.get_max_row_no(), max_col=self.get_max_col_no()):result.append([cell.value for cell in row])return result# 获取指定行数据def get_row_data(self, row_num):# 0 为标题行return [cell.value for cell in self.ws[row_num+1]]# 获取指定列数据def get_col_data(self, col_num):# 索引从0开始return [cell.value for cell in tuple(self.ws.columns)[col_num]]# 追加行数据且可以设置样式def write_row_data(self, data, font_color=None, border=True, fill_color=None):if not isinstance(data, (list, tuple)):print("写入数据失败:数据不为列表或元组类型!【%s】" % data)self.ws.append(data)# 设置字体颜色if font_color:if font_color.lower() in self.color_dict.keys():font_color = self.color_dict[font_color]# 设置单元格填充颜色if fill_color:if fill_color.lower() in self.color_dict.keys():fill_color = self.color_dict[fill_color]# 设置单元格边框if border:bd = Side(style="thin", color="000000")# 记录数据长度(否则会默认与之前行最长数据行的长度相同,导致样式超过了该行实际长度)count = 0for cell in self.ws[self.get_max_row_no()]:# 设置完该行的实际数据长度样式后,则退出if count > len(data) - 1:breakif font_color:cell.font = Font(color=font_color)# 如果没有设置字体颜色,则默认给执行结果添加字体颜色else:if cell.value is not None and isinstance(cell.value, str):if cell.value.lower() == "pass" or cell.value == "成功":cell.font = Font(color=self.color_dict["green"])elif cell.value.lower() == "fail" or cell.value == "失败":cell.font = Font(color=self.color_dict["red"])if border:cell.border = Border(left=bd, right=bd, top=bd, bottom=bd)if fill_color:cell.fill = PatternFill(fill_type="solid", fgColor=fill_color)count += 1# 指定行插入数据(行索引从0开始)def insert_row_data(self, row_no, data, font_color=None, border=True, fill_color=None):if not isinstance(data, (list, tuple)):print("写入数据失败:数据不为列表或元组类型!【%s】" % data)for idx, cell in enumerate(self.ws[row_no+1]):  # 此处行索引从1开始cell.value = data[idx]# 生成写入了测试结果的excel数据文件def save(self, save_file_name, timestamp):save_dir = os.path.join(TEST_REPORT_FILE_DIR, get_chinese_date())if not os.path.exists(save_dir):os.mkdir(save_dir)save_file = os.path.join(save_dir, save_file_name + "_" + timestamp + ".xlsx")self.wb.save(save_file)info("生成测试结果文件:%s" % save_file)return save_fileif __name__ == "__main__":from util.global_var import *from util.datetime_util import *excel = Excel(TEST_DATA_FILE_PATH)excel.get_sheet("测试结果统计")# print(excel.get_all_row_data())# print(excel.get_row_data(1))# print(excel.get_col_data(1))# excel.write_row_data(["4", None, "嘻哈"], "green", True, "red")excel.insert_row_data(1, [1,2,3])excel.save(get_timestamp())

ini_reader.py:

本模块封装了对 ini 配置文件的读取操作。

import configparserclass IniParser:# 初始化打开指定ini文件并指定编码def __init__(self, file_path):self.cf = configparser.ConfigParser()self.cf.read(file_path, encoding="utf-8")# 获取所有分组名称def get_sections(self):return self.cf.sections()# 获取指定分组的所有键def get_options(self, section):return self.cf.options(section)# 获取指定分组的键值对def get_items(self, section):return self.cf.items(section)# 获取指定分组的指定键的值def get_value(self, section, key):return self.cf.get(section, key)if __name__ == "__main__":from conf.global_var import *parser = IniParser(ELEMENT_FILE_PATH)print(parser.get_sections())print(parser.get_options("126mail_indexPage"))print(parser.get_value("126mail_indexPage", 'indexpage.frame'))

email_util.py:

本模块封装了邮件发送功能。(示例代码中的用户名/密码已隐藏)

import yagmail
import traceback
from util.log_util import *def send_mail(attachments_report_name, receiver, subject, content):try:# 连接邮箱服务器# 注意:若使用QQ邮箱,则password为授权码而非邮箱密码;使用其它邮箱则为邮箱密码# encoding设置为GBK,否则中文附件名会乱码yag = yagmail.SMTP(user="******@163.com", password="******", host="smtp.163.com", encoding='GBK')# 收件人、标题、正文、附件(若多个收件人或多个附件,则可使用列表)yag.send(to=receiver, subject=subject, contents=content, attachments=attachments_report_name)# 可简写:yag.send("****@163.com", subject, contents, report)info("测试报告邮件发送成功!【邮件标题:%s】【邮件附件:%s】【收件人:%s】" % (subject, attachments_report_name, receiver))except:error("测试报告邮件发送失败!【邮件标题:%s】【邮件附件:%s】【收件人:%s】" % (subject, attachments_report_name, receiver))error(traceback.format_exc())if __name__ == "__main__":send_mail("e:\\code.txt", "182230124@qq.com", "测试邮件", "正文")

datetime_util.py:

该模块实现了获取各种格式的当前日期时间。

import time# 返回中文格式的日期:xxxx年xx月xx日
def get_chinese_date():year = time.localtime().tm_yearif len(str(year)) == 1:year = "0" + str(year)month = time.localtime().tm_monif len(str(month)) == 1:month = "0" + str(month)day = time.localtime().tm_mdayif len(str(day)) == 1:day = "0" + str(day)return "{}年{}月{}日".format(year, month, day)# 返回英文格式的日期:xxxx/xx/xx
def get_english_date():year = time.localtime().tm_yearif len(str(year)) == 1:year = "0" + str(year)month = time.localtime().tm_monif len(str(month)) == 1:month = "0" + str(month)day = time.localtime().tm_mdayif len(str(day)) == 1:day = "0" + str(day)return "{}/{}/{}".format(year, month, day)# 返回中文格式的时间:xx时xx分xx秒
def get_chinese_time():hour = time.localtime().tm_hourif len(str(hour)) == 1:hour = "0" + str(hour)minute = time.localtime().tm_minif len(str(minute)) == 1:minute = "0" + str(minute)second = time.localtime().tm_secif len(str(second)) == 1:second = "0" + str(second)return "{}时{}分{}秒".format(hour, minute, second)# 返回英文格式的时间:xx:xx:xx
def get_english_time():hour = time.localtime().tm_hourif len(str(hour)) == 1:hour = "0" + str(hour)minute = time.localtime().tm_minif len(str(minute)) == 1:minute = "0" + str(minute)second = time.localtime().tm_secif len(str(second)) == 1:second = "0" + str(second)return "{}:{}:{}".format(hour, minute, second)# 返回中文格式的日期时间
def get_chinese_datetime():return get_chinese_date() + " " + get_chinese_time()# 返回英文格式的日期时间
def get_english_datetime():return get_english_date() + " " + get_english_time()# 返回时间戳
def get_timestamp():year = time.localtime().tm_yearif len(str(year)) == 1:year = "0" + str(year)month = time.localtime().tm_monif len(str(month)) == 1:month = "0" + str(month)day = time.localtime().tm_mdayif len(str(day)) == 1:day = "0" + str(day)hour = time.localtime().tm_hourif len(str(hour)) == 1:hour = "0" + str(hour)minute = time.localtime().tm_minif len(str(minute)) == 1:minute = "0" + str(minute)second = time.localtime().tm_secif len(str(second)) == 1:second = "0" + str(second)return "{}{}{}_{}{}{}".format(year, month, day, hour, minute, second)if __name__ == "__main__":print(get_chinese_datetime())print(get_english_datetime())

log_util.py:

该模块封装了日志打印输出、级别设定等功能。

import logging
import logging.config
from conf.global_var import *# 日志配置文件:多个logger,每个logger指定不同的handler
# handler:设定了日志输出行的格式
#          以及设定写日志到文件(是否回滚)?还是到屏幕
#          还定了打印日志的级别
logging.config.fileConfig(LOG_CONF_FILE_PATH)
logger = logging.getLogger("example01")def debug(message):logging.debug(message)def info(message):logging.info(message)def warning(message):logging.warning(message)def error(message):logging.error(message)if __name__=="__main__":debug("hi")info("hiphop")warning("hello")error("这是一个error日志")

report_util.py:

生成测试结果文件并发送邮件。

from util.email_util import send_mail
from util.datetime_util import *# 生成测试报告并发送邮件
def create_excel_report_and_send_email(excel_obj, receiver, subject, content):""":param excel_obj: excel对象用于保存文件:param timestamp: 用于文件命名的时间戳:return: 返回excel测试报告文件名"""time_stamp = get_timestamp()report_path = excel_obj.save(subject, time_stamp)send_mail(report_path, receiver, subject+"_"+time_stamp, content)

4. conf 目录

conf 目录属于第一层测试工具层,用于存储各配置文件。

elements_repository.ini:

该配置文件存储了各页面的元素对象的定位方式和定位表达式。

[126mail_indexPage]
indexPage.loginlink=xpath>//a[contains(text(),'密码登录')]
indexPage.frame=xpath>//iframe[contains(@id,'x-URS-iframe')]
indexPage.username=xpath>//input[@name='email']
indexPage.password=xpath>//input[@name='password']
indexPage.loginbutton=id>dologin[126mail_homePage]
homePage.addressLink=xpath>//div[text()='通讯录'][126mail_contactPersonPage]
contactPersonPage.createButton=xpath>//span[text()='新建联系人']
contactPersonPage.name=xpath>//a[@title='编辑详细姓名']/preceding-sibling::div/input
contactPersonPage.email=xpath>//*[@id='iaddress_MAIL_wrap']//input
contactPersonPage.starContacts=xpath>//span[text()='设为星标联系人']/preceding-sibling::span/b
contactPersonPage.phone=xpath>//*[@id='iaddress_TEL_wrap']//dd//input
contactPersonPage.otherinfo=xpath>//textarea
contactPersonPage.confirmButton=xpath>//span[.='确 定']

logger.conf:

###############################################
[loggers]
keys=root,example01,example02
[logger_root]
level=DEBUG
handlers=hand01,hand02[logger_example01]
handlers=hand01,hand02
qualname=example01
propagate=0[logger_example02]
handlers=hand01,hand03
qualname=example02
propagate=0###############################################
[handlers]
keys=hand01,hand02,hand03[handler_hand01]
class=StreamHandler
level=INFO
formatter=form01
args=(sys.stderr,)[handler_hand02]
class=FileHandler
level=DEBUG
formatter=form01
args=('E:\\pycharm_project_dir\\UIKeywordFramework\\log\\ui_test.log', 'a')[handler_hand03]
class=handlers.RotatingFileHandler
level=INFO
formatter=form01
args=('E:\\pycharm_project_dir\\UIKeywordFramework\\log\\ui_test.log', 'a', 10*1024*1024, 5)###############################################
[formatters]
keys=form01,form02[formatter_form01]
format=%(asctime)s [%(levelname)s] %(message)s
datefmt=%Y-%m-%d %H:%M:%S[formatter_form02]
format=%(name)-12s: [%(levelname)-8s] %(message)s
datefmt=%Y-%m-%d %H:%M:%S

5. test_data 目录

test_data 目录用于存放测试数据文件(Excel),存储了用例步骤、用例执行关键字、数据源等测试数据。

 

6. main.py

本模块是本框架的运行主入口,属于第四层“测试场景层”,将测试用例组织成测试场景,实现各种级别 cases 的管理,如冒烟,回归等测试场景。

  • 基于 business_process/main_process.py 中的模块用例 sheet 执行函数或主 sheet 执行函数,组装测试场景。
  • 可直接用代码组装测试场景,也可根据 excel 数据文件的用例集合和用例步骤的维护来设定测试场景。
  • 完成测试执行后生成测试结果文件并发送邮件。
from business_process.main_process import *
from util.report_util import *# 组装测试场景
# 冒烟测试
def smoke_test(report_name):excel, _ = suite_process(TEST_DATA_FILE_PATH, "登录(非数据驱动)")excel, _ = suite_process(excel, "关闭浏览器")# 生成测试报告并发送邮件create_excel_report_and_send_email(excel, ['itsjuno@163.com', '182230124@qq.com'], report_name, "请查收附件:UI自动化测试报告")# 全量测试:执行主sheet的用例集
def suite_test(report_name):excel = main_suite_process(TEST_DATA_FILE_PATH, "测试用例集")create_excel_report_and_send_email(excel, ['itsjuno@163.com', '182230124@qq.com'], report_name, "请查收附件:UI自动化测试报告")if __name__ == "__main__":# smoke_test("UI自动化测试报告_冒烟测试")suite_test("UI自动化测试报告_全量测试")

7. test_report 目录

本目录用于存放测试结果文件。

 

 

8. exception_pic 目录

本目录用于存放失败用例的截图。

9. log 目录

本目录用于存放日志输出文件(日志内容同时也会输出到控制台)。

log/ui_test.log:

五、PO模式

1、PO模式简介

PO(PageObject)设计模式将某个页面的所有元素对象定位和对元素对象的操作封装成一个 Page 类,并以页面为单位来写测试用例,实现页面对象和测试用例的分离。

PO 模式的设计思想与面向对象相似,能让测试代码变得可读性更好,可维护性高,复用性高。

PO 模式可以把一个页面分为三个层级:对象库层、操作层、业务层。

  1. 对象库层:封装定位元素的方法。

  2. 操作层:封装对元素的操作。

  3. 业务层:将一个或多个操作组合起来完成一个业务功能。

一条测试用例可能需要多个步骤操作元素,将每一个步骤单独封装成一个方法,在执行测试用例时调用封装好的方法进行操作。

PO 模式的优点:

  • 通过页面分层,将测试代码和被测试页面的页面元素及其操作方法进行分离,降低代码冗余。

  • 页面对象与用例分离,业务代码与测试代码分离,降低耦合性。

  • 不同层级分属不同用途,降低维护成本。

  • 代码可阅读性增强,整体流程更为清晰。

2、工程结构简介

整个测试框架分为四层,通过分层的方式,测试代码更容易理解,维护起来较为方便。

第一层是“测试工具层”:

  • util 包:用于实现测试过程中调用的工具类方法,例如读取配置文件、页面元素的操作方法、操作Excel文件等。
  • conf 包:配置文件及全局变量。
  • test_data 目录:Excel 数据文件,包含测试数据输入、测试结果输出。
  • log 目录:日志输出文件。
  • screenshot_path 目录:异常截图保存目录。

第二层是“服务层”,相当于对测试对象的一个业务封装。对于接口测试,是对远程方法的一个实现;对于页面测试,是对页面元素或操作的一个封装。

  • page 包:对象库层及操作层,将所有页面的元素对象定位及其操作分别封装成一个类。

第三层是“测试用例逻辑层”,该层主要是将服务层封装好的各个业务对象,组织成测试逻辑,进行校验。

  • action 包:组装单个用例的流程。
  • business_process 包:基于业务层和测试数据文件,执行测试用例集合。
  • test_data 目录:Excel 数据文件,包含测试数据输入、测试结果输出。 

第四层是“测试场景层”,将测试用例组织成测试场景,实现各种级别 cases 的管理、冒烟,回归等测试场景。 

  • main.py:本 PO 框架的运行主入口。

 

框架特点:

  1. 通过配置文件,实现页面元素定位方式和测试代码的分离。
  2. 使用 PO 模式,封装了网页中的页面元素,方便测试代码调用,也实现了一处维护全局生效的目标。
  3. 在 excel 文件中定义多组测试数据,每个登录用户都一一对应一个存放联系人数据的 sheet,测试框架可自动调用测试数据完成数据驱动测试。
  4. 实现了测试执行过程中的日志记录功能,可以通过日志文件分析测试脚本执行的情况。
  5. 在 excel 数据文件中,通过设定“测试数据是否执行”列的内容为 y 或 n,自定义选择测试数据,测试执行结束后会在"测试结果列"中显示测试执行的时间和结果,方便测试人员查看。

3、工程代码示例

1. page 包

对象库层及操作层,将所有页面的元素对象定位及其操作分别封装成一个类。

login_page.py:

from conf.global_var import *
from util.ini_parser import IniParser
from util.find_element_util import *# 登录页面元素定位及操作
class LoginPage:def __init__(self, driver):self.driver = driver# 初始化跳转登录页面self.driver.get(LOGIN_URL)# 初始化指定ini配置文件及指定分组self.cf = IniParser(ELEMENT_FILE_PATH, "126mail_loginPage")# 获取frame元素对象def get_frame_obj(self):locate_method, locate_exp = self.cf.get_value("loginPage.frame").split(">")return find_element(self.driver, locate_method, locate_exp)# 切换framedef switch_frame(self):self.driver.switch_to.frame(self.get_frame_obj())# 获取用户名输入框元素对象def get_username_input_obj(self):locate_method, locate_exp = self.cf.get_value("loginPage.username").split(">")return find_element(self.driver, locate_method, locate_exp)# 清空用户名输入框操作def clear_username(self):self.get_username_input_obj().clear()# 输入用户名操作def input_username(self, value):self.get_username_input_obj().send_keys(value)# 获取密码输入框元素对象def get_pwd_input_obj(self):locate_method, locate_exp = self.cf.get_value("loginPage.password").split(">")return find_element(self.driver, locate_method, locate_exp)# 输入密码操作def input_pwd(self, value):self.get_pwd_input_obj().send_keys(value)# 获取登录按钮对象def get_login_buttion_obj(self):locate_method, locate_exp = self.cf.get_value("loginPage.loginbutton").split(">")return find_element(self.driver, locate_method, locate_exp)# 点击登录按钮操作def click_login_button(self):self.get_login_buttion_obj().click()

home_page.py:

from conf.global_var import *
from util.ini_parser import IniParser
from util.find_element_util import *# 登录后主页元素定位及操作
class HomePage:def __init__(self, driver):self.driver = driver# 初始化指定ini配置文件及指定分组self.cf = IniParser(ELEMENT_FILE_PATH, "126mail_homePage")# 获取“通讯录”按钮对象def get_contact_button_obj(self):locate_method, locate_exp = self.cf.get_value("homePage.addressLink").split(">")return find_element(self.driver, locate_method, locate_exp)# 点击“通讯录”按钮def click_contact_button(self):self.get_contact_button_obj().click()

contact_page.py:

from conf.global_var import *
from util.ini_parser import IniParser
from util.find_element_util import *# 通讯录页面元素定位及操作
class ContactPage:def __init__(self, driver):self.driver = driver# 初始化指定ini配置文件及指定分组self.cf = IniParser(ELEMENT_FILE_PATH, "126mail_contactPersonPage")# 获取新建联系人按钮对象def get_contact_create_button_obj(self):locate_method, locate_exp = self.cf.get_value("contactPersonPage.createButton").split(">")return find_element(self.driver, locate_method, locate_exp)# 点击新建联系人按钮def click_contact_creat_button(self):self.get_contact_create_button_obj().click()# 获取姓名输入框对象def get_name_input_obj(self):locate_method, locate_exp = self.cf.get_value("contactPersonPage.name").split(">")return find_element(self.driver, locate_method, locate_exp)# 输入姓名操作def input_name(self, value):self.get_name_input_obj().send_keys(value)# 获取邮箱输入框对象def get_email_input_obj(self):locate_method, locate_exp = self.cf.get_value("contactPersonPage.email").split(">")return find_element(self.driver, locate_method, locate_exp)# 输入邮箱操作def input_email(self, value):self.get_email_input_obj().send_keys(value)# 获取星标联系人单选框对象def get_star_button_obj(self):locate_method, locate_exp = self.cf.get_value("contactPersonPage.starContacts").split(">")return find_element(self.driver, locate_method, locate_exp)# 点击星标联系人操作def click_star_button(self):self.get_star_button_obj().click()# 获取手机输入框对象def get_phone_input_obj(self):locate_method, locate_exp = self.cf.get_value("contactPersonPage.phone").split(">")return find_element(self.driver, locate_method, locate_exp)# 输入邮箱操作def input_phone(self, value):self.get_phone_input_obj().send_keys(value)# 获取备注输入框对象def get_remark_input_obj(self):locate_method, locate_exp = self.cf.get_value("contactPersonPage.otherinfo").split(">")return find_element(self.driver, locate_method, locate_exp)# 输入邮箱操作def input_remark(self, value):self.get_remark_input_obj().send_keys(value)# 获取确定按钮对象def get_confirm_button_obj(self):locate_method, locate_exp = self.cf.get_value("contactPersonPage.confirmButton").split(">")return find_element(self.driver, locate_method, locate_exp)# 点击星标联系人操作def click_confirm_button(self):self.get_confirm_button_obj().click()

2. action 包

业务层,将一个或多个操作组合起来完成一个业务功能。

case_action.py:

from selenium import webdriver
import traceback
import time
from page.contact_page import ContactPage
from page.home_page import HomePage
from page.login_page import LoginPage
from conf.global_var import *
from util.log_util import *# 初始化浏览器
def init_browser(browser_name):if browser_name.lower() == "chrome":driver = webdriver.Chrome(CHROME_DRIVER)elif browser_name.lower() == "firefox":driver = webdriver.Firefox(FIREFOX_DRIVER)elif browser_name.lower() == "ie":driver = webdriver.Ie(IE_DRIVER)else:return "Error browser name!"return driverdef assert_word(driver, text):assert text in driver.page_source# 登录流程封装
def login(driver, username, pwd, assert_text):login_page = LoginPage(driver)login_page.switch_frame()login_page.clear_username()login_page.input_username(username)login_page.input_pwd(pwd)login_page.click_login_button()time.sleep(1)assert_word(driver, assert_text)# 添加联系人流程封装
def add_contact(driver, name, email, phone, is_star, remark, assert_text):home_page = HomePage(driver)home_page.click_contact_button()contact_page = ContactPage(driver)contact_page.click_contact_creat_button()contact_page.input_name(name)contact_page.input_email(email)contact_page.input_phone(phone)contact_page.input_remark(remark)if is_star == "是":contact_page.click_star_button()contact_page.click_confirm_button()time.sleep(2)assert_word(driver, assert_text)def quit(driver):driver.quit()if __name__ == "__main__":driver = init_browser("chrome")login(driver, "zhangjun252950418", "zhangjun123", "退出")add_contact(driver, "铁蛋", "asfhi@123.com", "12222222222", "是", "这是备注", "铁蛋")# quit(driver)

3. business_process 包

基于业务层和测试文件,实现数据驱动的测试执行脚本。

batch_login_process.py:

from action.case_action import *
from util.excel_util import *
from conf.global_var import *
from util.datetime_util import *
from util.screenshot import take_screenshot# 封装测试数据文件中用例的执行逻辑
# 测试数据文件中的每个登录账号
def batch_login(test_data_file, browser_name, account_sheet_name):excel = Excel(test_data_file)# 获取登录账号sheet页数据excel.change_sheet(account_sheet_name)account_all_data = excel.get_all_row_data()account_headline_data = account_all_data[0]for account_row_data in account_all_data[1:]:# 执行登录用例account_row_data[ACCOUNT_TEST_TIME_COL] = get_english_datetime()if account_row_data[ACCOUNT_IS_EXECUTE_COL].lower() == "n":continue# 初始化浏览器driver = init_browser(browser_name)try:# 默认以"退出"作为断言关键字login(driver, account_row_data[ACCOUNT_USERNAME_COL], account_row_data[ACCOUNT_PWD_COL], "退出")info("登录成功【用户名:{}, 密码:{}, 断言关键字:{}】".format(account_row_data[ACCOUNT_USERNAME_COL],account_row_data[ACCOUNT_PWD_COL], "退出"))account_row_data[ACCOUNT_TEST_RESULT_COL] = "pass"except:error("登录失败【用户名:{}, 密码:{}, 断言关键字:{}】".format(account_row_data[ACCOUNT_USERNAME_COL],account_row_data[ACCOUNT_PWD_COL], "退出"))account_row_data[ACCOUNT_TEST_RESULT_COL] = "fail"account_row_data[ACCOUNT_TEST_EXCEPTION_INFO_COL] = traceback.format_exc()account_row_data[ACCOUNT_SCREENSHOT_COL] = take_screenshot(driver)# 写入登录用例的测试结果excel.change_sheet("测试结果")excel.write_row_data(account_headline_data, "red")excel.write_row_data(account_row_data)excel.save()# 切换另一个账号时需先关闭浏览器,否则会自动登录driver.quit()if __name__ == "__main__":batch_login(TEST_DATA_FILE_PATH, "chrome", "126账号")

 batch_login_and_add_contact_process.py:

from action.case_action import *
from util.excel_util import *
from conf.global_var import *
from util.datetime_util import *
from util.screenshot import take_screenshot# 封装测试数据文件中用例的执行逻辑
# 测试数据文件中每个登录账号下,添加所有联系人数据
def batch_login_and_add_contact(test_data_file, browser_name, account_sheet_name):excel = Excel(test_data_file)# 获取登录账号sheet页数据excel.change_sheet(account_sheet_name)account_all_data = excel.get_all_row_data()account_headline_data = account_all_data[0]for account_row_data in account_all_data[1:]:# 执行登录用例account_row_data[ACCOUNT_TEST_TIME_COL] = get_english_datetime()if account_row_data[ACCOUNT_IS_EXECUTE_COL].lower() == "n":continue# 初始化浏览器driver = init_browser(browser_name)# 获取联系人数据sheetcontact_data_sheet = account_row_data[ACCOUNT_DATA_SHEET_COL]try:# 默认以"退出"作为断言关键字login(driver, account_row_data[ACCOUNT_USERNAME_COL], account_row_data[ACCOUNT_PWD_COL], "退出")info("登录成功【用户名:{}, 密码:{}, 断言关键字:{}】".format(account_row_data[ACCOUNT_USERNAME_COL],account_row_data[ACCOUNT_PWD_COL], "退出"))account_row_data[ACCOUNT_TEST_RESULT_COL] = "pass"except:error("登录失败【用户名:{}, 密码:{}, 断言关键字:{}】".format(account_row_data[ACCOUNT_USERNAME_COL],account_row_data[ACCOUNT_PWD_COL], "退出"))account_row_data[ACCOUNT_TEST_RESULT_COL] = "fail"account_row_data[ACCOUNT_TEST_EXCEPTION_INFO_COL] = traceback.format_exc()account_row_data[ACCOUNT_SCREENSHOT_COL] = take_screenshot(driver)# 写入登录用例的测试结果excel.change_sheet("测试结果")excel.write_row_data(account_headline_data, "red")excel.write_row_data(account_row_data)excel.save()# 执行添加联系人用例excel.change_sheet(contact_data_sheet)contact_all_data = excel.get_all_row_data()contact_headline_data = contact_all_data[0]# 在测试结果中,一个账号下的联系人数据标题行仅写一次contact_headline_flag = Truefor contact_row_data in contact_all_data[1:]:if contact_row_data[CONTACT_IS_EXECUTE_COL].lower() == "n":continuecontact_row_data[CONTACT_TEST_TIME_COL] = get_english_datetime()try:add_contact(driver, contact_row_data[CONTACT_NAME_COL], contact_row_data[CONTACT_EMAIL_COL],contact_row_data[CONTACT_PHONE_COL], contact_row_data[CONTACT_IS_STAR_COL],contact_row_data[CONTACT_REMARK_COL], contact_row_data[CONTACT_ASSERT_KEYWORD_COL])info("添加联系人成功【姓名:{}, 邮箱:{}, 手机号:{}, 是否星标联系人:{}, ""备注:{}, 断言关键字:{}】".format(contact_row_data[CONTACT_NAME_COL], contact_row_data[CONTACT_EMAIL_COL],contact_row_data[CONTACT_PHONE_COL], contact_row_data[CONTACT_IS_STAR_COL],contact_row_data[CONTACT_REMARK_COL], contact_row_data[CONTACT_ASSERT_KEYWORD_COL]))contact_row_data[CONTACT_TEST_RESULT_COL] = "pass"except:error("添加联系人失败【姓名:{}, 邮箱:{}, 手机号:{}, 是否星标联系人:{}, ""备注:{}, 断言关键字:{}】".format(contact_row_data[CONTACT_NAME_COL], contact_row_data[CONTACT_EMAIL_COL],contact_row_data[CONTACT_PHONE_COL], contact_row_data[CONTACT_IS_STAR_COL],contact_row_data[CONTACT_REMARK_COL], contact_row_data[CONTACT_ASSERT_KEYWORD_COL]))contact_row_data[CONTACT_TEST_RESULT_COL] = "fail"contact_row_data[CONTACT_TEST_EXCEPTION_INFO_COL] = traceback.format_exc()contact_row_data[CONTACT_SCREENSHOT_COL] = take_screenshot(driver)# 写入登录用例的测试结果excel.change_sheet("测试结果")if contact_headline_flag:excel.write_row_data(contact_headline_data, "red")contact_headline_flag = Falseexcel.write_row_data(contact_row_data)excel.save()# 切换另一个账号时需先关闭浏览器,否则会自动登录driver.quit()if __name__ == "__main__":batch_login_and_add_contact(TEST_DATA_FILE_PATH, "chrome", "126账号")

4. util 包

用于实现测试过程中调用的工具类方法,例如读取配置文件、页面元素的操作方法、操作Excel文件等。

excel_util.py:(openpyxl 版本:3.0.4)

from openpyxl import load_workbook
from openpyxl.styles import PatternFill, Font, Side, Border
import osclass Excel:def __init__(self, test_data_file_path):# 文件格式校验if not os.path.exists(test_data_file_path):print("Excel工具类初始化失败:【{}】文件不存在!".format(test_data_file_path))returnif not test_data_file_path.endswith(".xlsx") or not test_data_file_path.endswith(".xlsx"):print("Excel工具类初始化失败:【{}】文件非excel文件类型!".format(test_data_file_path))return# 打开指定excel文件self.wb = load_workbook(test_data_file_path)# 初始化默认sheetself.ws = self.wb.active# 保存文件时使用的文件路径self.test_data_file_path = test_data_file_path# 初始化红、绿色,供样式使用self.color_dict = {"red": "FFFF3030", "green": "FF008B00"}# 查看所有sheet名称def get_sheets(self):return self.wb.sheetnames# 根据sheet名称切换sheetdef change_sheet(self, sheet_name):if sheet_name not in self.get_sheets():print("sheet切换失败:【{}】指定sheet名称不存在!".format(sheet_name))returnself.ws = self.wb.get_sheet_by_name(sheet_name)# 返回当前sheet的最大行号def max_row_num(self):return self.ws.max_row# 返回当前sheet的最大列号def max_col_num(self):return self.ws.max_column# 获取指定行数据(设定索引从0开始)def get_one_row_data(self, row_no):if row_no < 0 or row_no > self.max_row_num()-1:print("输入的行号【{}】有误:需在0至最大行数之间!".format(row_no))return# API的索引从1开始return [cell.value for cell in self.ws[row_no+1]]# 获取指定列数据def get_one_col_data(self, col_no):if col_no < 0 or col_no > self.max_col_num()-1:print("输入的列号【{}】有误:需在0至最大列数之间!".format(col_no))returnreturn [cell.value for cell in tuple(self.ws.columns)[col_no+1]]# 获取当前sheet的所有行数据def get_all_row_data(self):result = []# # API的索引从1开始for row_data in self.ws[1:self.max_row_num()]:result.append([cell.value if cell.value is not None else "" for cell in row_data])return result# 追加一行数据def write_row_data(self, data, fill_color=None, font_color=None, border=True):if not isinstance(data, (list, tuple)):print("追加的数据类型有误:需为列号或元组类型!【{}】".format(data))returnself.ws.append(data)# 添加字体颜色if font_color:if font_color in self.color_dict.keys():font_color = self.color_dict[font_color]# 需要设置的单元格长度应与数据长度一致,否则默认与之前行的长度一致count = 0for cell in self.ws[self.max_row_num()]:if count > len(data) - 1:break# cell不为None,才能设置样式if cell:if cell.value in ["pass", "成功"]:cell.font = Font(color=self.color_dict["green"])elif cell.value in ["fail", "失败"]:cell.font = Font(color=self.color_dict["red"])else:cell.font = Font(color=font_color)count += 1# 添加背景颜色if fill_color:if fill_color in self.color_dict.keys():fill_color = self.color_dict[fill_color]count = 0for cell in self.ws[self.max_row_num()]:if count > len(data) - 1:breakif cell:cell.fill = PatternFill(fill_type="solid", fgColor=fill_color)count += 1# 添加单元格边框if border:bd = Side(style="thin", color="000000")count = 0for cell in self.ws[self.max_row_num()]:if count > len(data) - 1:breakif cell:cell.border = Border(left=bd, right=bd, top=bd, bottom=bd)count += 1# 保存文件def save(self):self.wb.save(self.test_data_file_path)if __name__ == "__main__":from conf.global_var import *excel = Excel(TEST_DATA_FILE_PATH)excel.change_sheet("登录1")# print(excel.get_all_row_data())excel.write_row_data((1,2,"嘻哈",None,"ddd"), "red", "green")excel.save()

find_element_util.py:

from selenium.webdriver.support.ui import WebDriverWait# 显式等待一个对象
def find_element(driver, locate_method, locate_exp):# 显式等待对象(最多等10秒,每0.2秒判断一次等待的条件)return WebDriverWait(driver, 10, 0.2).until(lambda x: x.find_element(locate_method, locate_exp))# 显式等待一组对象
def find_elements(driver, locate_method, locate_exp):# 显式等待对象(最多等10秒,每0.2秒判断一次等待的条件)return WebDriverWait(driver, 10, 0.2).until(lambda x: x.find_elements(locate_method, locate_exp))

ini_parser.py:

import configparserclass IniParser:# 初始化打开指定ini文件并指定编码def __init__(self, file_path, section):self.cf = configparser.ConfigParser()self.cf.read(file_path, encoding="utf-8")self.section = section# 获取所有分组名称def get_sections(self):return self.cf.sections()# 获取指定分组的所有键def get_options(self):return self.cf.options(self.section)# 获取指定分组的键值对def get_items(self):return self.cf.items(self.section)# 获取指定分组的指定键的值def get_value(self, key):return self.cf.get(self.section, key)

datetime_util.py:

import time# 返回中文格式的日期:xxxx年xx月xx日
def get_chinese_date():year = time.localtime().tm_yearif len(str(year)) == 1:year = "0" + str(year)month = time.localtime().tm_monif len(str(month)) == 1:month = "0" + str(month)day = time.localtime().tm_mdayif len(str(day)) == 1:day = "0" + str(day)return "{}年{}月{}日".format(year, month, day)# 返回英文格式的日期:xxxx/xx/xx
def get_english_date():year = time.localtime().tm_yearif len(str(year)) == 1:year = "0" + str(year)month = time.localtime().tm_monif len(str(month)) == 1:month = "0" + str(month)day = time.localtime().tm_mdayif len(str(day)) == 1:day = "0" + str(day)return "{}/{}/{}".format(year, month, day)# 返回中文格式的时间:xx时xx分xx秒
def get_chinese_time():hour = time.localtime().tm_hourif len(str(hour)) == 1:hour = "0" + str(hour)minute = time.localtime().tm_minif len(str(minute)) == 1:minute = "0" + str(minute)second = time.localtime().tm_secif len(str(second)) == 1:second = "0" + str(second)return "{}时{}分{}秒".format(hour, minute, second)# 返回英文格式的时间:xx:xx:xx
def get_english_time():hour = time.localtime().tm_hourif len(str(hour)) == 1:hour = "0" + str(hour)minute = time.localtime().tm_minif len(str(minute)) == 1:minute = "0" + str(minute)second = time.localtime().tm_secif len(str(second)) == 1:second = "0" + str(second)return "{}:{}:{}".format(hour, minute, second)# 返回中文格式的日期时间
def get_chinese_datetime():return get_chinese_date() + " " + get_chinese_time()# 返回英文格式的日期时间
def get_english_datetime():return get_english_date() + " " + get_english_time()if __name__ == "__main__":print(get_chinese_datetime())print(get_english_datetime())

log_util.py:

import logging
import logging.config
from conf.global_var import *# 日志配置文件:多个logger,每个logger指定不同的handler
# handler:设定了日志输出行的格式
#          以及设定写日志到文件(是否回滚)?还是到屏幕
#          还定了打印日志的级别
logging.config.fileConfig(LOG_CONF_FILE_PATH)
logger = logging.getLogger("example01")def debug(message):logging.debug(message)def info(message):logging.info(message)def warning(message):logging.warning(message)def error(message):logging.error(message)if __name__ == "__main__":debug("hi")info("gloryroad")warning("hello")error("这是一个error日志")

screenshot.py:

import traceback
import os
from util.datetime_util import *
from conf.global_var import *# 截图函数
def take_screenshot(driver):# 创建当前日期目录dir = os.path.join(SCREENSHOT_PATH, get_chinese_date())if not os.path.exists(dir):os.makedirs(dir)# 以当前时间为文件名file_name = get_chinese_time()file_path = os.path.join(dir, file_name+".png")try:driver.get_screenshot_as_file(file_path)# 返回截图文件的绝对路径return file_pathexcept:print("截图发生异常【{}】".format(file_path))traceback.print_exc()return file_path

5. conf 包

配置文件及全局变量。

elements_repository.ini:

[126mail_loginPage]
loginPage.frame=xpath>//iframe[contains(@id,'x-URS-iframe')]
loginPage.username=xpath>//input[@name='email']
loginPage.password=xpath>//input[@name='password']
loginPage.loginbutton=id>dologin[126mail_homePage]
homePage.addressLink=xpath>//div[text()='通讯录'][126mail_contactPersonPage]
contactPersonPage.createButton=xpath>//span[text()='新建联系人']
contactPersonPage.name=xpath>//a[@title='编辑详细姓名']/preceding-sibling::div/input
contactPersonPage.email=xpath>//*[@id='iaddress_MAIL_wrap']//input
contactPersonPage.starContacts=xpath>//span[text()='设为星标联系人']/preceding-sibling::span/b
contactPersonPage.phone=xpath>//*[@id='iaddress_TEL_wrap']//dd//input
contactPersonPage.otherinfo=xpath>//textarea
contactPersonPage.confirmButton=xpath>//span[.='确 定']

global_var.py:

import os# 工程根路径
PROJECT_ROOT_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))# 元素定位方法的ini配置文件路径
ELEMENT_FILE_PATH = os.path.join(PROJECT_ROOT_PATH, "conf", "elements_repository.ini")# 驱动路径
CHROME_DRIVER = "E:\\auto_test_driver\\chromedriver.exe"
IE_DRIVER = "E:\\auto_test_driver\\IEDriverServer.exe"
FIREFOX_DRIVER = "E:\\auto_test_driver\\geckodriver.exe"# 测试使用的浏览器
BROWSER_NAME = "chrome"# 登录主页
LOGIN_URL = "https://mail.126.com"# 日志配置文件路径
LOG_CONF_FILE_PATH = os.path.join(PROJECT_ROOT_PATH, "conf", "logger.conf")# 测试用例文件路径
TEST_DATA_FILE_PATH = os.path.join(PROJECT_ROOT_PATH, "test_data", "测试用例.xlsx")# 截图保存路径
SCREENSHOT_PATH = os.path.join(PROJECT_ROOT_PATH, "screenshot_path")# 单元测试报告输出目录
UNITTEST_REPORT_PATH = os.path.join(PROJECT_ROOT_PATH, "report")# 登录账号sheet页数据列号
ACCOUNT_USERNAME_COL = 1
ACCOUNT_PWD_COL = 2
ACCOUNT_DATA_SHEET_COL = 3
ACCOUNT_IS_EXECUTE_COL = 4
ACCOUNT_TEST_TIME_COL = 5
ACCOUNT_TEST_RESULT_COL = 6
ACCOUNT_TEST_EXCEPTION_INFO_COL = 7
ACCOUNT_SCREENSHOT_COL = 8# 联系人sheet页数据列号
CONTACT_NAME_COL = 1
CONTACT_EMAIL_COL = 2
CONTACT_IS_STAR_COL = 3
CONTACT_PHONE_COL = 4
CONTACT_REMARK_COL = 5
CONTACT_ASSERT_KEYWORD_COL = 6
CONTACT_IS_EXECUTE_COL = 7
CONTACT_TEST_TIME_COL = 8
CONTACT_TEST_RESULT_COL = 9
CONTACT_TEST_EXCEPTION_INFO_COL = 10
CONTACT_SCREENSHOT_COL = 11if __name__ == "__main__":print(PROJECT_ROOT_PATH)

logger.conf

###############################################
[loggers]
keys=root,example01,example02
[logger_root]
level=DEBUG
handlers=hand01,hand02[logger_example01]
handlers=hand01,hand02
qualname=example01
propagate=0[logger_example02]
handlers=hand01,hand03
qualname=example02
propagate=0###############################################
[handlers]
keys=hand01,hand02,hand03[handler_hand01]
class=StreamHandler
level=INFO
formatter=form01
args=(sys.stderr,)[handler_hand02]
class=FileHandler
level=DEBUG
formatter=form01
args=('.\\log\\126_mail_test.log', 'a')[handler_hand03]
class=handlers.RotatingFileHandler
level=INFO
formatter=form01
args=('.\\log\\126_mail_test.log', 'a', 10*1024*1024, 5)###############################################
[formatters]
keys=form01,form02[formatter_form01]
format=%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s
datefmt=%Y-%m-%d %H:%M:%S[formatter_form02]
format=%(name)-12s: %(levelname)-8s %(message)s
datefmt=%Y-%m-%d %H:%M:%S

6. test_data 目录

测试用例.xlsx:包含测试数据输入、测试结果输出:

7. log 目录

日志输出文件:126_mail_test.log

...
...
2021-02-23 16:59:15 log_util.py[line:19] INFO 登录成功【用户名:zhangjun252950418, 密码:zhangjun123, 断言关键字:退出】
2021-02-23 16:59:20 log_util.py[line:19] INFO 添加联系人成功【姓名:lily, 邮箱:lily@qq.com, 手机号:135xxxxxxx1, 是否星标联系人:是, 备注:常联系人, 断言关键字:lily@qq.com】
2021-02-23 16:59:24 log_util.py[line:27] ERROR 添加联系人失败【姓名:张三, 邮箱:zhangsan@qq.com, 手机号:158xxxxxxx3, 是否星标联系人:否, 备注:不常联系人, 断言关键字:zhangsan@qq.comxx】
2021-02-23 16:59:27 log_util.py[line:19] INFO 添加联系人成功【姓名:李四, 邮箱:lisi@qq.com, 手机号:157xxxxxx9, 是否星标联系人:否, 备注:, 断言关键字:李四】
...
...

8. screenshot_path 目录

异常截图保存目录:

9. main.py

本 PO 框架的运行主入口。

from business_process.batch_login import *
from business_process.batch_login_and_add_contact import *
from conf.global_var import *# 示例组装:冒烟测试
def smoke_test():batch_login(TEST_DATA_FILE_PATH, "chrome", "126账号")# 示例组装:全量测试
def full_test():batch_login_and_add_contact(TEST_DATA_FILE_PATH, "chrome", "126账号")if __name__ == "__main__":# smoke_test()full_test()

六、自动化单元测试

unittest是Python语言的单元测试框架,在Python的官方文档中,对unittest单元测试框架进行了详细的介绍,感兴趣的朋友可以到 https://www.python.org/doc/网站了解。本章重点介绍unittest单元测试框架在自动化测试中的应用。

unittest 单元测试框架提供了创建测试用例、测试套件和批量执行测试用例的方案。在python 安装成功后,unittest 单元测试框架就可以直接导入使用,它属于标准库。作为单元测试的框架,unittest 单元测试框架也是对程序的最小模块进行的一种敏捷化测试。在自动化测试中,我们虽然不需要做白盒测试,但是必须知道所使用语言的单元测试框架,这是因为当我们把Selenium2的API全部学习完后,就会遇到用例的组织问题。虽然函数式编程和面向对象编程提供了对代码的重构,但是对于所编写的每个测试用例,不可能编写成一个函数(方法)来调用执行。

利用单元测试框架,可以创建一个类,该类继承 unittest 的TestCase,这样可以把每个TestCase 看成是一个最小的单元,由测试套件组织起来,运行时直接执行即可,同时可引入测试报告。unittest各个组件的关系如图所示。

1、测试固件

在untitest 单元测试框架中,测试固件用于处理初始化的操作,例如,在对百度的搜索进行测试之前,首先需要打开浏览器并且进入到百度首页;测试结束后,需要关闭浏览器。测试固件提供了两种执行形式,一种是每执行一个测试用例,测试固件都会被执行到;另外一种是不管有多少个测试用例,测试固件只执行一次。

1. 测试固件每次均执行

unittest单元测试框架提供了名为 setUp和tearDown的测试固件。下面,我们通过编写一个例子来看测试固件执行的方式,测试代码为: 

#!/usr/bin/env python 
#-*-coding:utf-8-*-import unittest
class BaiduTest(unittest.TestCase):def setUp(self):print('start')def tearDown(self):print('end')def test baidu_so(self):print('测试用例执行')if __name__ == '__main__':unittest.main(verbosity=2)

它的执行顺序是先执行 setUp 方法,再执行具体的测试用例 test_baidu_so,最后执行tearDown方法。

执行后的结果如图所示:

下面以百度首页为例,编写两个测试点。执行的方式是 setUp 执行两次,tearDown也会执行两次,具体实现的代码如下: 

#!/usr/bin/env python 、
#-*-coding:utf-8-*-from selenium import webdriver import unittestclass BaiduTest(unittest.TestCase):def setUp(self):self.driver=webdriver.Firefox()self.driver.maximize_window()self.driver.get('http://www.baidu.com')self.driver.implicitly_wait(30)def tearDown(self):self.driver.quit()def test_baidu_news(self):'''验证∶测试百度首页点击新闻后的跳转'''self.driver.find_element_by_link_text('新闻').click()def test_baidu_map(self):'''验证∶测试百度首页点击地图后的跳转'''self.driver.find_element_by_link_text('地图').click()if ___name___ == '__main__':unittest.main(verbosity=2)

运行以上代码后,浏览器会被打开两次,也会关闭两次。如果是在一个测试类中有N个测试用例,那么也就意味着打开N次浏览器,关闭N次浏览器,而关闭和打开浏览器都会占用一定的资源和时间,很显然,这并不是一个理想的选择。

2. 测试固件只执行一次

钩子方法setUp和tearDown虽然经常使用,但是在UI自动化测试中,一个系统的测试用例一般多达五百多条,打开和关闭五百多次浏览器,会消耗大量的资源和时间。在unittest 单元测试框架中可以使用另外一个测试固件来解决这一问题,它就是 setUpClass和tearDownClass方法。该测试固件方法是类方法,需要在方法上面加装饰器@classmethod。使用该测试固件,不管有多少个测试用例,测试固件只执行一次,也就是说不管有多少个测试用例,执行的时候浏览器只会被打开一次和关闭一次,案例代码如下:

#!/usr/bin/env python 
#-*-coding:utf-8-*-import  unittestclass UiTest(unittest.TestCase):@classmethoddef setUpClass(cls):print('start')@classmethoddef tearDownClass(cls):print('end')def test_001(self):print('第一个测试用例')def test_002(self):print('第二个测试用例')if __name__ == '__main__':unittest.main(verbosity=2)

执行以上测试代码后,输出的结果如图所示。

注解:在以上代码中,虽然有两个测试方法,但是测试固件只执行了一次。

虽然使用类测试固件可以在执行多个测试用例时让测试固件只执行一次,但是实际使用类测试固件中,还需要解决另外一个问题。例如,以百度首页测试点击新闻页面和测试点击地图页面为例,意味着点击新闻页面后,需回到首页后才可以找得到地图页面的链接进行点击。

因为在新闻页面并没有地图的链接地址,从而导致地图页面的测试失败,反之亦然。

实例代码如下: 

#!/usr/bin/env python 
#-*-coding:utf-8-*-from selenium import webdriver 
import unittestclass BaiduTest(unittest.TestCase):@classmethoddef setUpClass(cls):cls.driver=webdriver.Firefox()cls.driver.maximize_window()cls.driver.get('http://www.baidu.com')cls.driver.implicitly_wait(30)@classmethoddef tearDownClass(cls):cls.driver.quit()def test baidu news(self):'''验证∶测试百度首页点击新闻后的跳转'''self.driver.find_element_by_link_text('新闻').click()self.driver.get('http://www.baidu.com')def test baidu map(self):'''验证∶测试百度首页点击地图后的跳转'''self.driver.find_element_by_link_text('地图').click()self.driver.get('http://www.baidu.com')if __name__ == '__main__':unittest.main(verbosity=2)

运行以上代码后,所有的测试用例执行通过。在实际的自动化测试工作中,建议大家尽量使用测试固件setUp和tearDown,使得自动化测试用例之间没有关联性,避免一个测试用例执行失败是由上一个测试用例导致。

2、测试执行

在以上实例中,可以看到测试用例的执行是在主函数中,unittest 调用的是main,代码如下:

main = TestProgram

TestProgram还是一个类,再来看该类的构造函数,代码如下:

class TestProgram(object):
"""A command-line program that runs a set of tests; this is primarily
for making test modules conveniently executable. 
"""
USAGE = USAGE_FROM_MODULE# defaults for testing
failfast = catchbreak = buffer = progName = Nonedef __init__(self, module='_main_', defaultTest=None, argv=None, testRunner=None, testLoader=loader.defaultTestLoader, exit=True,verbosity=1,failfast=None, catchbreak=None, buffer=None):if isinstance(module,basestring):self.module = __import__(module)for part in module.split('.')[1:]:self.module = getattr(self.module, part)else:self.module = module if argv is None:argv = sys.argvself.exit = exit
self.failfast = failfast 
self.catchbreak = catchbreak 
self.verbosity=verbosity
self.buffer = buffer
self.defaultTest = defaultTest 
self.testRunner = testRunner 
self.testLoader=testLoader
self.progName = os.path.basename(argv[0])
self.parseArgs(argv)
self.runTests()

在unittest模块中包含的main方法,可以方便地将测试模块转变为可以运行的测试脚本。main 使用 unittest.TestLoader类来自动查找和加载模块内的测试用例,TestProgram类中该部分的代码如下:

def createTests(self):if self.testNames is None:self.test = self.testLoader.loadTestsFromModule(self.module)else:self.test = self.testLoader.loadTestsFromNames(self.testNames,self.module)

在执行测试用例时,在main方法中加入了verbosity=2,代码如下:

unittest.main(verbosity=2)

下面详细地解释一下verbosity部分。在verbosity中默认是1,0代表执行的测试总数和全局结果,2代表显示详细的信息。

我们现在通过一个实例来说明执行结果的显示,实例代码如下:

#!/usr/bin/env python #coding:utf-8import unittest
from selenium import webdriverclass Baidu(unittest.TestCase):def setUp(self):self.driver=webdriver.Chrome()self.driver.maximize window()self.driver.implicitly_wait(30)self.driver.get('http://www.baidu.com')def tearDown(self):self.driver.quit()def test baidu_title(self):'''验证百度首页的title'''self.assertEqual(self.driver.title,'百度一下,你就知道')def test baidu url(self):'''验证百度首页的URL'''self.assertEqual(self.driver.current_url,'https://ww.baidu.com')if __name__ == '__main__':unittest.main(verbosity=2)

执行后的结果如图所示: 

注解:在以上的截图中可以看到,成功的测试用例会显示 OK,失败的测试用例会显示出详细的信息。

在一个测试类中,有很多的测试用例,如果想单独地执行某一个测试,用鼠标右键点击要执行的测试用例的名称,选择“Run”。如我们想单独执行test_baidu_news的TestCase,将鼠标移动到该TestCase名称上用右键点击,再在出现的菜单项中点击Run “Unittest test_baidu_news”,如图所示。 

点击Run后,就会单独地执行test_baidu_news的测试用例。 

3、构建测试套件

前面介绍了测试用例的执行,在一个测试类中会有很多个测试用例。如何来组织并使用这些测试用例呢?untitest 提供了“测试套件”方法,它由 unittest 模块中的TestSuite 类表示,测试套件可以根据所测试的特性把测试用例组合在一起。

1. 按顺序执行

在实际的工作中,由于业务场景需要测试用例按顺序执行,例如,先执行X测试用例再执行Y测试用例,在TestSuite类中提供了addTest方法可以实现,也就是说要执行的测试用例按自己期望的执行顺序添加到测试套件中。下面的案例实现对百度首页的测试,测试用例的执行顺序是先测试百度新闻,再测试百度地图,代码如下:

#!/usr/bin/env python 
#-*-coding:utf-8-*-
from selenium import webdriver 
import unittestclass BaiduTest(unittest.TestCase):def setUp(self):self.driver=webdriver.Firefox()self.driver.maximize_window()self.driver.get('http://www.baidu.com')self.driver.implicitly_wait(30)def tearDown(self):self.driver.quit()def test baidu_news(self):'''验证∶测试百度首页点击新闻后的跳转'''self.driver.find_element_by_link_text('新闻').click()url=self.driver.current_urlself.assertEqual(url,'http://news.baidu.com/')def test baidu map(self):'''验证∶测试百度首页点击地图后的跳转'''self.driver.find_element_by_link_text('地图').click()self.driver.get('http://news.baidu.com/')if __name__ == '__main__':suite=unittest.TestSuite()suite.addTest(BaiduTest('test_baidu_news'))suite.addTest(BaiduTest('test_baidu_map'))unittest.TextTestRunner(verbosinyao).run(suite)

注解:在以上代码中,首先需要对 TestSuite 类进行实例化,使之成为一个对象suite,然后调用TestSuite类中addTest方法,把测试用例添加到测试套件中,最后执行测试套件,从而执行测试套件中的测试用例。

运行以上代码后,测试用例会按照添加到测试套件的顺序执行,也就是说先添加进去的先执行,后添加进去的后执行。

2. 按测试类执行

在自动化测试中,一般测试用例往往多达几百个,如果完全按顺序来执行,其一是不符合自动化测试用例的原则,因为在UI 自动化测试中,自动化测试用例最好独立执行,互相之间不影响并且没有依赖关系。其二是当一个测试类中有很多测试用例时,逐一地向套件中添加用例是一项很烦琐的工作,这时,可以使用makeSuite类按测试类来执行。makeSuite可以实现把测试用例类中所有的测试用例组成测试套件 TestSuite 这样可避免逐一向测试套件中添加测试用例。

修改后的测试类中的代码如下:

#!/usr/bin/env python 
#-*-coding:utf-8-*-from selenium import webdriver 
import unittestclass BaiduTest(unittest.TestCase):def setUp(self):self.driver=webdriver.Firefox()self.driver.maximize_window()self.driver.get('http://www.baidu.com')self.driver.implicitly_wait(30)def tearDown(self):self.driver.quit()def test_baidu_news(self):'''验证∶测试百度首页点击新闻后的跳转'''self.driver.find_element_by_link_text('新闻').click()url=self.driver.current_urlself.assertEqual(url,'http://news.baidu.com/')def test baidu map(self):'''验证∶测试百度首页点击地图后的跳转'''self.driver.find_element_by_link_text('地图').click()self.driver.get('http://www.baidu.com')if __name__ == '__main__':suite=unittest.TestSuite(unittest.makeSuite(BaiduTest)unittest.TextTestRunner(verbosity=2).run(suite)

注解:在以上代码中可以看到,在测试套件 TestSuite 类中,unittest 模块调用了makeSuite的方法,makeSuite方法的参数是 testCaseClass,也就是测试类,代码如下:

def makeSuite(testCaseClass, prefix='test', sortUsing=cmp, suiteClass=suite.TestSuite):return  makeLoader(prefix, sortUsing, suiteClass).loadTestsFromTestCase(testCaseClass)

3. 加载测试类

在unittest 模块中也可以使用 TestLoader 类来加载测试类,也就是说TestLoader加载测试类并将它们返回添加到TestSuite中,TestLoader类的应用代码如下:

#!/usr/bin/env python 
#-*-coding:utf-8-*-from selenium import webdriver 
import unittestclass BaiduTest(unittest.TestCase):def setUp(self):self.driver=webdriver.Firefox()self.driver.maximize window()self.driver.get('http://www.baidu.com')self.driver.implicitly_wait(30)def tearDown(self):self.driver.quit()def test baidu_news(self):'''验证∶测试百度首页点击新闻后的跳转'''self.driver.find_element_by_link_text('新闻').click()url=self.driver.current_urlself.assertEqual(url,'http://news.baidu.com/')def test baidu map(self):'''验证∶测试百度首页点击地图后的跳转'''self.driver.find_element_by_link_text('地图').click()            self.driver.get('http://www.baidu.com')if __name__ == '__main__':suite=unittest.TestLoader().loadTestsFromTestCase(BaiduTest)unittest.TextTestRunner(verbosity=2).run(suite)

4. 按测试模块执行

在TestLoader类中也可以按模块来执行测试。在Python中,一个Python文件就是一个模块,一个模块中可以有 N个测试类,在一个测试类中可以有 N个测试用例。

按模块执行的代码如下:

#!/usr/bin/env python 
#-*-coding:utf-8-*-
from selenium import webdriver 
import unittestclass BaiduTest(unittest.TestCase):def setUp(self):self.driver=webdriver.Firefox()self.driver.implicitly_wait(30)self.driver.get('http://www.baidu.com')def test title(self):'''验证∶测试百度浏览器的title'''self.assertEqual(self.driver.title,'百度一下,你就知道')def test_so(self):'''验证∶测试百度搜索输入框是否可编辑'''so=self.driver.find_element_by_id('kw')self.assertTrue(so.is_enabled()def test_002(self):
'''验证点击百度新闻'''self.driver.find_element_by_link_text('新闻').click()@unittest.skip('do not run')
def test_003(self):'''验证∶点击百度地图'''self.driver.find_element_by_link_text('地图').click()def tearDown(self):self.driver.quit()class BaiduMap(unittest.TestCase):def setUp(self):self.driver = webdriver.Firefox()self.driver.maximize_window()self.driver.get('http://www.baidu.com')self.driver.implicitly_wait(30)def tearDown(self):self.driver.quit()def test_baidu_map(self):'''验证∶测试百度首页点击地图后的跳转'''self.driver.find_element_by_link_text('地图').click()        self.driver.get('http://www.baidu.com')if __name__ == '__main__':suite=unittest.TestLoader().loadTestsFromModule('unittest1.py')unittest.TextTestRunner(verbosity=2).run(suite)

注解:在以上代码中可以看到,测试类分别是BaiduMap和BaiduTest,模块名称为unittest1.py,TestLoader类直接调用loadTestsFromModule方法返回给指定模块中包含的所有测试用例套件。

5. 优化测试套件

#!/usr/bin/env python 
#-*-coding:utf-8-*-from selenium import webdriver \import unittest
class BaiduTest(unittest.TestCase):def setUp(self):self.driver=webdriver.Firefox()self.driver.maximize_window()self.driver.get('http://www.baidu.com')self.driver.implicitly_wait(30)def tearDown(self):self.driver.quit()def test baidu_news(self):'''验证∶测试百度首页点击新闻后的跳转'''self.driver.find_element_by_link_text('新闻').click()self.assertEqual(self.driver.current_url,'http://news.baidu.com/')def test baidu map(self):'''验证∶测试百度首页点击地图后的跳转'''self.driver.find element_by_link_text('地图').click()self.assertEqual(self.driver.current_url,'https://map.baidu.com/')@staticmethoddef suite(testCaseClass):suite=unittest.TestLoader().loadTestsFromTestCase(testCaseClass)return suiteif __name__ == '__main__':unittest.TextTestRunner(verbosity=2).run(BaiduTest.suite(BaiduTest))

4、分离测试固件

在UI 自动化测试中,不管编写哪个模块的测试用例,都需要首先在测试类中编写测试固件初始化WebDriver类及打开浏览器,测试用例执行完成后还需要关闭浏览器,这部分的代码如下:

def setUp(self):self.driver=webdriver.Firefox()self.driver.maximize window()self.driver.get('http://www.baidu.com')self.driver.implicitly_wait(30)def tearDown(self):self.driver.quit()

在每一个测试类中都要编写以上代码,因此需要重复编写很多代码。是否可以把测试固件这部分代码分离出去,测试类直接继承分离出去的类呢?我们把测试固件分离到init.py模块中,类名称为InitTest,代码如下:

#!/usr/bin/env python 
#-*-coding:utf-8-*-import unittest
from selenium import webdriverclass InitTest(unittest.TestCase):def setUp(self):self.driver = webdriver.Firefox()self.driver.maximize_window()self.driver.get('http://www.baidu.com')self.driver.implicitly_wait(30)def tearDown(self):self.driver.quit()

测试类继承了InitTest,继承后,在测试类中直接编写要执行的测试用例,代码如下:

#!/usr/bin/env python 
#-*-coding:utf-8-*-import unittest
from init import InitTestclass BaiduTest(InitTest):def test baidu news(self):'''验证∶测试百度首页点击新闻后的跳转'''self.driver.find_element_by_link_text('新闻').click()self.assertEqual(self.driver.current url,'http://news.baidu.com/')def test baidu map(self):'''验证∶测试百度首页点击地图后的跳转'''self.driver.find_element_by_link_text('地图').click()      self.assertEqual(self.driver.current_url,'https://map.baidu.com/')if name == '__main__':unittest.main(verbosity=2)

注解:首先需要导入 init 模块中的InitTest 类,测试类 BaiduTest 继承InitTest类。这样执行测试类后,会先执行setUp方法,再执行具体的测试用例,最后执行 tearDown 方法。python 的类继承的方式解决了在每个测试类中都需要编写测试固件的问题。把测试固件分离出去后,即使后期测试地址发生变化,只需要修改init模块中InitTest类中的url地址即可,而不需要在每个测试类修改测试地址,减少了编写重复性代码的开销。

分离了测试固件,运行以上代码,对应的测试用例执行通过。

5、测试断言

断言就是判断实际测试结果与预期结果是否一致,一致则测试通过,否则失败。因此,在自动化测试中,无断言的测试用例是无效的。这是因为当一个功能自动化已全部实现,在每次版本迭代中执行测试用例时,执行的结果必须是权威的,也就是说自动化测试用例执行结果应该无功能性或者逻辑性问题。在自动化测试中最忌讳的就是自动化测试的用例功能测试虽然是通过的,但被测功能本身却是存在问题的。

自动化测试用例经常应用在回归测试中,发现的问题不是特别多,如果测试结果存在功能上的问题,则投入了人力去做的自动化测试就没有多大的意义了。所以每一个测试用例必须要有断言。在测试的结果中只有两种可能,一种是执行通过,另外一种是执行失败,也就是功能存在问题。在TestCase类中提供了assert方法来检查和报告失败,常用的方法如图所示。

1. assertEqual

assertEqual 方法用于测试两个值是否相等,如果不相等则测试失败。在这里特别强调的是,两个值相等不仅仅指内容相同而且还要类型相同。例如,两个值虽然内容一致,但一个是bytes类型一个是str类型,则两个值仍为不相等,测试失败。下面通过一个实例来演示,代码如下:

#!/usr/bin/env python 
#-*-coding:utf-8-*-import unittest
from selenium import webdriverclass BaiduTest(unittest.TestCase):def setUp(self):self.driver=webdriver.Firefox()self.driver.implicitly_wait(30)self.driver.get('http://www.baidu.com')def tearDown(self):self.driver.quit()def test baidu_title(self):'''验证∶测试百度首页的title'''self.assertEqual(self.driver.title,'百度一下,你就知道'.encode('gbk')if __name__ == '__main__':unittest.main(verbosity=2)

以上代码会执行失败,这是因为 self.driver.title 获取的内容是“百度一下,你就知道”,它的类型是str类型;而“百度一下,你就知道”被转为bytes类型,内容一致、类型不一致,所以失败,执行后的结果如图所示。

我们把代码再次修改一下,让两个断言内容一致,类型也一致,修改后的代码如下: 

import unittest
from selenium import webdriverclass BaiduTest(unittest.TestCase):def setUp(self):self.driver=webdriver.Firefox()self.driver.implicitly_wait(30)self.driver.get('http://www.baidu.com')def tearDown(self):self.driver.quit()def test baidu_title(self):'''验证∶测试百度首页的title'''self.assertEqual(self.driver.title,'百度一下,你就知道')if __name__ == '__main__':unittest.main(verbosity=2)

再次运行以上代码,测试用例执行成功,断言通过。

2. assertTrue

assertTrue 返回的是布尔类型,它主要对返回的测试结果执行布尔类型的校验。例如,验证百度搜索输入框是否可编辑,is_enabled 方法返回的结果是 True,实现的代码如下:

#!/usr/bin/env python 
#-*-coding:utf-8-*-import unittest
from init import InitTestclass BaiduTest(InitTest):def test_baidu_news(self):'''验证∶测试百度搜索输入框是否可编辑'''so=self.driver.find_element_by_id('kw')self.assertTrue(so.is_enabled()if __name__ == '__main__':unittest.main(verbosity=2)

3. assertFalse

assertFalse和assertTrue,都是对返回的布尔类型进行校验。不同的是,assertFalse要求返回的结果是 False,测试用例才会执行成功。以新浪邮箱登录页面为例,取消“自动登录”按钮后,方法 is_elected返回的是 False,实现的代码如下:

#!/usr/bin/env python 
#-*-coding:utf-8-*-import unittest
from init import InitTestclass BaiduTest(InitTest):def test baidu news(self):'''验证∶新浪邮箱登录页面取消自动登录'''isautoLogin=self.driver.find_element_by_id('storel')isautoLogin.click()self.assertFalse(isautoLogin.is_selected()if __name__ == '__main__':unittest.main(verbosity=2)

4. assertIn

assertIn 指的是一个值是否包含在另外一个值的范围内。还是以百度首页的url为例,测试https://www.baidu.com/是否在https://www.baidu.com/的范围内,实现的代码如下:

#!/usr/bin/env python 
#-*-coding:utf-8-*-import unittest
from init import  InitTestclass BaiduTest(InitTest):def test baidu news(self):'''验证∶新浪邮箱登录页面取消自动登录'''self.assertIn(self.driver.current_url,'https://www.baidu.com/')if __name__ == '__main__':unittest.main(verbosity=2)

6、断言的注意事项

前面介绍了unittest 测试框架中断言的基本内容,之所以单独列出一节来讲断言的注意事项,这是因为在实际的工作过程中,以及在与其他的网友交流时,发现很多人的测试用例写得不够规范,导致即使产品的功能有问题,测试用例执行结果仍为正确。出现这种情况,多是因为在测试过程中,使用了不正确的if应用和异常应用。下面就从这两个维度来解答这个错误信息是如何发生的,以及怎么样来避免它。

1. 不正确的if应用

还是以新浪登录页面为例,来测试自动登录按钮是否选中,该代码中引入了判断的代码如下:

#!/usr/bin/env python 
#-*-coding:utf-8-*-import  unittest
from selenium import webdriverclass BaiduTest(unittest.TestCase):def setUp(self):self.driver = webdriver.Firefox()self.driver.maximize_window()self.driver.get('http://mail.sina.com.cn/')self.driver.implicitly_wait(30)def tearDown(self):self.driver.quit()def test sina login(self):'''验证∶新浪登录页面取消自动登录'''isAutoLogin=self.driver.find_element_by_id('storel')if isAutoLogin.is_selected():print 'succcess'else:print 'fail'if __name__ == '__main__':unittest.main(verbosity=2)

注解:一个测试用例只有两种结果,要么 Pass,要么Fail(代码错误也显示Fail)。以上代码不管复选框是不是自动选中的,测试执行结果都是 Pass,测试用例都是通过。因此,在自动化测试的测试用例中,切记不要使用if else这类判断代码来代替断言。

2. 不正确的异常应用

这里还是以新浪登录页面为例,先看以下实例代码:

#!/usr/bin/env python 
#-*-coding:utf-8-*-import unittest
from selenium import webdriverclass BaiduTest(unittest.TestCase):def setUp(self):self.driver = webdriver.Firefox()self.driver.maximize_window()self.driver.get('http://mail.sina.com.cn/')self.driver.implicitly_wait(30)def tearDown(self):self.driver.quit()def test sina login(self):'''验证∶新浪登录页面取消自动登录'''isAutoLogin=self.driver.find_element_by_id('storel')try:self.assertTrue(isAutoLogin.is selected())except:print 'fail'if __name__ == '__main__':unittest.main(verbosity=2)

注解:以上代码和应用if else的结果一样,即使自动登录未被选中,测试用例的结果也显示Pass。另外,在自动化测试中尽量不要应用打印结果来判断测试用例的情况,用例如,果在代码错误或者功能有 Bug 的情况下就让用例报错或者失败,而不是结果显示Pass,只有功能正常的测试用例结果才是Pass的。

7、批量执行测试用例

在实际测试中,常常需要批量执行测试用例。例如,在testCase 包中有test_baidu.py和test_sina.py两个文件,下面批量执行这两个模块的测试用例。创建新文件allTests.py,在allTests.py文件中编写批量执行的代码,test_baidu.py模块的代码如下:

#!/usr/bin/env python 
#-*-coding:utf-8-*-import unittest
from selenium import webdriverclass BaiduTest(unittest.TestCase):def setUp(self):self.driver = webdriver.Firefox()self.driver.maximize window()self.driver.get('http://www.baidu.com')self.driver.implicitly_wait(30)def tearDown(self):self.driver.quit()def test baidu_title(self):'''验证∶测试百度首页的title是否正确'''self.assertEqual(self.driver.title,'百度一下,你就知道')if __name__ == '__main__':unittest.main(verbosity=2)

test_sina.py模块代码如下:

#!/usr/bin/env python 
#-*-coding:utf-8-*-import unittest
from selenium import webdriverclass SinaTest(unittest.TestCase):def setUp(self):self.driver = webdriver.Firefox()self.driver.maximize_window()self.driver.get('http://mail.sina.com.cn/')self.driver.implicitly_wait(30)def tearDown(self):self.driver.quit()def test_username_password_null(self):'''验证∶新浪登录页面用户名和密码为空错误提示信息'''self.driver.find_element_by_id('freename').send_keys('')self.driver.find_element_by_id('freepassword').send_keys('')self.driver.find_element_by_link_text('登录').click()divError=self.driver.find_element_by_xpath('html/body/div[1]/div[2]/div/div/div[4]/div[1]/div[1]/div[1]/span[1]').text self.assertEqual(divError,'请输入邮箱名')if __name__ == '__main__':unittest.main(verbosity=2)

allTests.py模块的代码如下:

#!/usr/bin/env python 
#-*-coding:utf-8-*-import unittest
import  osdef allcases():'''获取所有测试模块'''suite=unittest.TestLoader().discover(start_dir= os.path.dirname(__file__), pattern='test_*.py', top_level_dir=None)return suiteif __name__ =='__main__':unittest.TextTestRunner(verbosity=2).run(allCases())

注解:在以上代码中,批量获取测试模块用到的方法是 discover。discover方法有三个参数,第一个参数 start_dir是测试模块的路径,存放在testCase包中;第二个参数pattern用来获取testCase包中所有以test开头的模块文件,会获取到test_baidu.py和test_sina.py;第三个参数 top_level_dir 在调用的时候直接给默认值None。

discover方法的代码如下:

def discover(self,start_dir,pattern='test*.py',top_level_dir=None):

运行以上allTests.py文件后,测试结果如图所示。

8、生成测试报告

运行 allTests.py 文件后得到的测试结果不够专业,无法直接提交,因此需要借助第三方库生成HTML格式的测试报告。这里用到的库是HTMLTestRunner.py,下载地址是:
GitHub - tungwaiyip/HTMLTestRunner: HTMLTestRunner is an extension to the Python standard library's unittest module. It generates easy to use HTML test reports.

下载 HTMLTestRunner.py文件后,把该文件放到 Python安装路径的Lib子文件夹中,例如,C:\Python36-32\Lib目录下。创建report文件夹,与testCase包放在同一个目录下,继续完善 allTests.py 文件,最终生成测试报告,最终的allTests.py代码如下: 

#!/usr/bin/env python #-*-coding:utf-8-*-import unittest import  os
import  HTMLTestRunner import timedef allTests():'''获取所有要执行的测试用例'''suite=unittest.defaultTestLoader.discover(start_dir=os.path.join(os.path.dirname(_file_),'testCase'), pattern='test_*.py', top_level_dir=None)return suitedef getNowTime():'''获取当前的时间'''return time.strftime('%Y-%m-%d %H_%M_%S',time.localtime(time.time())def run():fileName=os.path.join(os.path.dirname(_file_),'report', getNowTime()+'report.html')fp=open(fileName,'wb')runner=HTMLTestRunner.HTMLTestRunner(stream=fp,title='UI自动化测试报告',description='UI自动化测试报告详细信息')
runner.run(allTests())if __name__ == '__main__'
run()

注解:在以上完善后的allTests.py 文件中其中导入了sys、HTMLTestRunner库。getNowTime 方法用来获取当前时间,每一次生成的测试报告如果文件名称一致,由于加上了最新时间信息,便可以根据文件名称确认哪个是最新的测试报告;run 方法用来执行测试套件中的测试用例和生成测试报告。针对HTMLTestRunner.py 文件,在Python3中需要对代码进行修改,否则,执行allTests.py后就会报错。

再次运行 allTests.py 文件后,在report 文件夹下生成了最新的测试报告,report的目录如图所示。

打开该HTML文件,可以看到基于HTML的测试报告,如图所示。

注解:在图4-9-2所示的基于HTML的测试报告中,我们可以看到开始时间、执行时间、测试用例通过数、失败数、错误数,点击Detail还可以看到更详细的信息,如图所示。 

9、代码覆盖率统计实战

Coverage.py 是 Python 程序代码覆盖率的测试工具,用于监视程序执行了哪些代码,未执行哪些代码。在Python3中,首先需要通过pip3 install coverage来安装它,安装过程如图所示。

安装coverage后运行allTests.py文件,程序会运行所有以test开头的测试模块的文件。到allTests.py模块的路径下运行以下代码:

coverage3 run allTests.py

再次执行 coverage html,也就是代码覆盖率统计,通过 html 的文件查看,执行的命令如图所示。

执行后,在Chapter4目录下会生成一个htmlcov文件夹,在该文件夹里面显示的是代码覆盖率统计的文件,如图所示。 

点击打开 index.html 文件,显示的是每个文件运行代码的覆盖率统计,如图所示。 

点击任意一个模块文件,就会显示该模块执行的代码,如点击 test_sina.py文件,代码如图所示。 

七、UI自动化测试三方库扩展

1、读取ini配置文件

1. 配置项

在每个 ini 配置文件中,配置数据会被分组(比如下述配置文件中的“config”和“cmd”),每个分组中又可以指定对应的变量值。

示例:test.ini

# 定义config分组
[config]
platformName=Android
appPackage=com.romwe
appActivity=com.romwe.SplashActivity# 定义cmd分组
[cmd]
viewPhone=adb devices
startServer=adb start-server
stopServer=adb kill-server# 定义log分组
[log]
log_error=true

读取API:

  • read(filename):直接读取文件内容
  • sections():得到所有的 section,并以列表的形式返回
  • options(section):得到该 section 的所有 option
  • items(section):得到该 section 的所有键值对
  • get(section,option):得到 section 中 option 的值,返回为 string 类型
  • getint(section, option):得到 section 中 option 的值,返回为 int 类型
  • getboolean()
  • getfloat()
>>> import configparser
>>>
>>> cf = configparser.ConfigParser()
>>> cf.read("e:\\db.ini", encoding="utf-8")  # 读取配置文件
['e:\\db.ini']
>>>
>>> cf.sections()  # 获取各分组
['config', 'cmd', 'log']
>>>
>>> cf.options("config")  # 获取指定分组的所有键
['platformname', 'apppackage', 'appactivity']
>>>
>>> cf.items("config")  # 获取指定分组的所有键值对
[('platformname', 'Android'), ('apppackage', 'com.romwe'), ('appactivity', 'com.romwe.SplashActivity')]
>>>
>>> cf.get("log", "log_error")  # 获取指定分组及指定键的值
'true'

配置文件中的键是不区分大小写的,例如下述两种方式是等价的:

cf.get("config", "appActivity")
cf.get("config", "APPACTIVITY")

在解析时,getboolean()方法查找任何可行的值,例如以下几个都是等价的:

[log]
log_error=true
log_error=TRUE
log_error=1
log_error=yes

2、写入API

  • write(fp):将config对象写入至某个 .init 格式的文件
  • add_section(section):添加一个新的 section
  • set(section, option, value):对 section 中的 option 进行设置,需要调用 write 将内容写入配置文件
  • remove_section(section):删除某个 section
  • remove_option(section, option):删除某个 sction 的某个 key
>>> import configparser
>>>
>>> cf = configparser.ConfigParser()
>>>
>>> cf.add_section("section1")  # 添加分组
>>> cf.set("section1", "key1", "value1")  # 添加键值对
>>> cf.set("section1", "key2", "value2")
>>>
>>> cf.add_section("section2")
>>> cf.set("section2", "key3", "value3")
>>>
>>> cf.add_section("section3")
>>> cf.set("section3", "key4", "value4")
>>> cf.set("section3", "key5", "value5")
>>>
>>> cf.remove_section("section2")  # 删除指定分组
True
>>> cf.remove_option("section3", "key4")  # 删除指定分组的指定键值对
True
>>>
>>> cf.sections()
['section1', 'section3']
>>>
>>> f = open("config.ini", "w")
>>> cf.write(f)  # 保存配置文件
>>> f.flush()  # 刷新或关闭文件,数据才会真正写入文件
>>> f.close()

3、UI 对象库

UiObjectMap.ini:

用配置文件存储页面元素的定位方式和定位表达式,做到定位数据和程序的分离。

[sogou]
searchBox = id>query
searchButton = id>stb

ObjectMap.py:

ObjectMap 工具类文件,供测试程序调用。

from selenium.webdriver.support.ui import WebDriverWait
import configparser
import os
from selenium import webdriverclass ObjectMap:def __init__(self):# 获取存放页面元素定位方式及定位表达式配置文件路径# os.path.abspath(__file__):获取当前文件所在路径目录self.uiObjectMapPath = os.path.dirname(os.path.abspath(__file__)) + "\\UiObjectMap.ini"print(self.uiObjectMapPath)  # E:\eclipse-workspace\AutoTest\UI\UiObjectMap.inidef getElementObject(self, driver, webSiteName, elementName):try:# 创建一个读取配置文件的实例cf = configparser.ConfigParser()# 将配置文件加载到内存cf.read(self.uiObjectMapPath)# 根据section和option获取配置文件中页面元素的定位方式及定位表达式组成的字符串,# 并使用">"分隔locators = cf.get(webSiteName, elementName).split(">")# 得到定位方式locatorMethod = locators[0]# 得到定位表达式locatorExpression = locators[1]print(locatorMethod, locatorExpression)# 显式等待获取页面元素element = WebDriverWait(driver, 10).until(lambda x: x.find_element(locatorMethod, locatorExpression))except Exception as e:print(e)else:# 找到页面元素,返回给调用者return element

SoGou.py:

调用 ObjectMap 工具类实现测试逻辑。

from selenium import webdriver
import unittest
import time, traceback
from UI.ObjectMap import ObjectMapclass TestSoGouObjectMap(unittest.TestCase):def setUp(self):self.obj = ObjectMap()self.driver = webdriver.Firefox()def tearDown(self):self.driver.quit()def testSoGouSearch(self):url = "http://www.sogou.com";self.driver.get(url)try:# 查找页面的搜索输入框searchBox = self.obj.getElementObject(self.driver, "sogou", "searchBox")searchBox.send_keys("WebDriver")# 查找搜索按钮searchButton = self.obj.getElementObject(self.driver, "sogou", "searchButton")searchButton.click()time.sleep(2)self.assertTrue("Selenium" in self.driver.page_source, "assert error!")except Exception:print(traceback.print_exc())if __name__ == "__main__":unittest.main()

4、xlrd、xlwt 库 

xlrd 模块实现对 excel 文件内容读取,xlwt 模块实现对 excel 文件的写入。

模块安装:

pip install xlrd  # 新版仅支持 .xls。若要支持 .xlsx 可安装旧版 pip install xlrd==1.2.0
pip install xlwt

1. xlrd

import xlrd# 获取excel文件薄对象
wb = xlrd.open_workbook("e:\\test.xlsx")# 获取所有sheet名称
sheet_name = wb.sheet_names()
print(sheet_name)# 根据sheet索引获取sheet对象(从0开始)
sheet = wb.sheet_by_index(0)
# 根据sheet名获取sheet对象
sheet = wb.sheet_by_name("业务场景")
# 获取sheet对象的名称、行数和列数
print("sheet_name:{}, nrows:{}, ncols:{}".format(sheet.name, sheet.nrows, sheet.ncols))# 获取整行和整列的值(返回列表)
print(sheet.row_values(0))  # 获取第一行的数据
print(sheet.col_values(1))  # 获取第二列的数据# 获取指定单元格的值
print(sheet.cell(0, 1).value)  # 获取第1行第2列的值
print(sheet.cell_value(0, 1))  # 获取第1行第2列的值
print(sheet.row(0)[1].value)  # 获取第1行第2列的值# 获取单元格值的数据类型
print(sheet.cell(0, 1).ctype)
# ctype 值说明: 0 empty, 1 string, 2 number, 3 date, 4 boolean, 5 error# 获取日期类型的单元格值
from datetime import date
if sheet.cell(2, 1).ctype == 3:print(sheet.cell(2, 1).value)  # 44089.0date_value = xlrd.xldate_as_tuple(sheet.cell(2, 1).value, wb.datemode)print(date_value)  # (2020, 9, 15, 0, 0, 0)print(date(*date_value[:3]))  # 2020-09-15print(date(*date_value[:3]).strftime("%Y/%m/%d"))  # 2020/09/15# 将number类型的单元格值转为整型
if sheet.cell(0, 0).ctype == 2:print(sheet.cell(0, 0).value)  # 123.0print(int(sheet.cell(0, 0).value))# 获取合并单元格的值(需要merged_cells属性)# 需要在读取excel文件的时候添加参数formatting_info,并设置为True(默认是False)
# 否则可能调用merged_cells属性获取到的是空值
wb2 = xlrd.open_workbook("e:\\test.xls", formatting_info=True)
# 注意:在读取xlsx格式的Excel时,传入formatting_info会直接抛出异常,而读取xls类型的文件时不存在此问题。
# 抛异常的原因是formatting_info还没有对新版本的xlsx的格式完成兼容
sheet2 = wb2.sheet_by_name("业务场景")
print(sheet2.merged_cells)  # [(0, 1, 0, 8), (2, 6, 0, 1)]
# merged_cells返回的这四个参数的含义是:(row,row_range,col,col_range)
# 分别表示:开始的行索引、结束的行索引(不包含)、开始的列索引、结束的列索引(不包含)
# (0, 1, 0, 8) 表示第1行的1-8列 合并
# (2, 6, 0, 1) 表示3-6行的1-2列 合并# 分别获取合并2个单元格的内容:
# 规律:获取merge_cells返回的row和col低位的索引即可。
print(sheet2.cell(0, 0).value)
print(sheet2.cell_value(2, 0))# 使用以下方法更加方便
merge_value = []
for (row, row_range, col, col_range) in sheet2.merged_cells:merge_value.append((row, col))
for v in merge_value:print(sheet2.cell(v[0], v[1]).value)

2. xlwt

import xlwt
from datetime import datetime# 封装样式设置函数
def set_style(font_name, height, bold=False, format_str=""):# 初始化样式style = xlwt.XFStyle()# 初始化字体font = xlwt.Font()font.name = font_name  # 如:Times New Romanfont.bold = boldfont.height = height# 初始化边框borders = xlwt.Borders()borders.left = 6borders.right = 6borders.top = 6borders.bottom = 6# 为样式赋值字体、边框style.font = fontstyle.borders = bordersstyle.num_format_str = format_strreturn style# 创建workbook对象
wb = xlwt.Workbook()
# 创建sheet(缺少指定sheet的方法)
ws = wb.add_sheet("New Sheet", cell_overwrite_ok=True)# 设置第一列列宽
ws.col(0).wodth = 200*30# 写入普通单元格
# 分别传参:行索引、列索引、需要写入的值、样式设置函数
ws.write(0, 0, "cell value", set_style("Time New Roman", 220, bold=True, format_str="#,##0.00"))
ws.write(0, 1, datetime.now(), set_style("Time New Roman", 220, bold=True, format_str="DD-MM-YYYY"))
# 值为时间类型时,format_str有如下选项:
# D-MMM-YY, D-MMM-YY, D-MMM, MMM-YY, h:mm, h:mm:ss, h:mm, h:mm:ss, M/D/YY h:mm, mm:ss, [h]:mm:ss, mm:ss.0# 写入合并单元格
# 参数含义:开始的行下标、结束的行下标(包含)、始的列下标、结束的列下标(包含)、写入的内容
# 列合并:写入第2行,第2~5列
ws.write_merge(1, 1, 1, 4, "列合并")
# 行合并:写入第1~3行,第3列
ws.write_merge(0, 2, 2, 2, "行合并")# 添加新样式
styleOK = xlwt.easyxf("pattern: fore_colour light_blue;font: colour green, bold True;")
# 实例化样式类
pattern = xlwt.Pattern()
pattern.pattern = xlwt.Pattern.SOLID_PATTERN  # 固定样式
pattern.pattern_fore_colour = xlwt.Style.colour_map["red"]  # 背景颜色
styleOK.pattern = patternws.write(6, 0, 1, style=styleOK)
ws.write(6, 1, 1)
ws.write(6, 2, xlwt.Formula("A3+B3"))# 保存(仅支持xls)
wb.save("e:\\test_xlwt.xls")

5、openpyxl 库 

openpyxl 是一款比较综合的工具,不仅能够同时读取和修改 Excel 文档,而且可以对 Excel 文件内单元格进行详细设置,包括单元格样式等内容,甚至还支持图表插入、打印设置等内容。使用 openpyxl 可以读写 xltm、xltx、xlsm、xlsx 等类型的文件,且可以处理数据量较大的 Excel 文件,跨平台处理大量数据是其它模块没法相比的。因此,openpyxl 成为处理 Excel 复杂问题的首选库函数。

openpyxl 与 xlrd/xlwt 的比较

两者都是对于 excel 文件操作的模块,其主要区别在于:

  • 写操作:
    • xlwt:针对E Excel 2007 之前的版本(.xls),无法生成 xlsx 文件。
    • openpyxl:主要针对 Excel 2007 之后的版本(.xlsx)。
  • 读写速度:
    • xlrd/xlwt 在读写方面的速度都要优于 openpyxl。
  • 文件大小:
    • xlrd/xlwt:对单个 sheet 不超过 65535 行。
    • openpyxl:对文件大小没有限制。

所以想要尽量提高效率又不影响结果时,可以考虑用 xlrd 读取,用 openpyxl 写入。

具体的使用流程如下:

  1. 导入 openpyxl 模块;
  2. 调用 openpyxl.load_workbook() 函数或 openpyxl.Workbook(),取得 Workbook 对象;
  3. 调用 get_active_sheet() 或 get_sheet_by_name() 工作簿方法,取得 Worksheet 对象;
  4. 使用索引或工作表的 cell() 方法,带上 row 和 column 关键字参数,取得 Cell 对象,读取或编辑 Cell 对象的 value 属性。

1. 获取 workbook 与 sheet 对象

创建新文件:

from openpyxl import Workbook# 创建工作簿对象
wb = Workbook()# 激活sheet;拿到当前文件对象中默认操作的一个sheet,如上次关闭文件时所打开的sheet
ws = wb.active
# 表格在被创建的时候会自动的有一个名字。它们被命名在一个队列中(sheet, ...),可以使用title属性在任何时候来改变它们的名字。
ws.title = "tmp"# 删除sheet
wb.remove(ws)# 创建sheet
ws1 = wb.create_sheet("tmp1")  # 默认在最后插入新sheet
ws2 = wb.create_sheet("tmp2", 0)  # 在索引为0的位置插入# 保存文件(覆盖同名文件的全部内容)
wb.save("文件名称.xlsx")

打开已有文件:

# 打开已有文件
from openpyxl import load_workbook# 打开指定文件
wb = load_workbook("e:\\test.xlsx")# 查看所有sheet名
print(wb.sheetnames)  # 返回列表
# 遍历所有sheet名
for sheet in wb.sheetnames:print(sheet.title())# 选择sheet
ws3 = wb["tmp3"]  # 方法1:名称可以作为key进行查找
ws4 = wb.get_sheet_by_name("tmp4")  # 方法2
print(ws3 is ws4)  # True# 在原有内容上进行修改并保存
wb.save("e:\\test.xlsx")

2. 访问单元格及其值

注意:当一个工作表在内存中被创建时,它里面默认是没有表格对象的,它们只有在第一次被访问的时候才会被创建,从而减少内存占用。因为这个特性,我们要循环表格而不是直接访问它们,这样会将所有的表格对象在内存中创建,就算你没有访问它们中的任何一个值。

openpyxl 读写单元格时,单元格的坐标位置起始值是(1,1),即下标最小值为1。

访问单个单元格:

# 获取最大行列(返回数值)
print(ws3.max_row)
print(ws3.max_column)# 方法1:指定行列
print(ws3["A2"].value)# 方法2:指定行列
print(ws3.cell(row=2, column=2).value)  # 行号和列号从1开始# 方法3:只要访问就会创建对应单元格对象
for i in range(1, 10):for j in range(1, 10):print(ws4.cell(i, j).value)

行列序号转换:

1 from openpyxl.utils import get_column_letter, column_index_from_string
2 
3 # 根据列的数字返回字母
4 print(get_column_letter(2))  # B
5 # 根据字母返回列的数字
6 print(column_index_from_string('D'))  # 4

访问多个单元格:

# 访问指定行数据print(ws3[1])  # 方法1:索引从1开始
print(ws3[1:3])  # 切片方式,返回二维元组
print(tuple(ws3.rows)[1])  # 方法2:索引从0开始,sheet.rows为生成器, 里面是每一行的数据,每一行又由一个tuple包裹
# 遍历获取每个单元格的值
for cell in ws3[1]:print(cell.value)# 访问指定列数据print(ws3["A"])
print(tuple(ws3.columns)[1])  # 访问第2列单元格
print(ws3["A:C"])  # 返回二维元组# 指定范围# 方法1
print(ws3["A1:B4"])
# 方法2:最多访问两行两列的单元格
for row in ws3.iter_rows(min_row=1, max_row=2, max_col=2):  # 行号和列号从1开始for cell in row:print(cell)
'''执行结果:




'''for row in ws3.iter_cols(min_row=1, max_row=2, max_col=2):  # 行号和列号从1开始for cell in row:print(cell)
'''注意与上述iter_rows的获取顺序不同




'''

矩阵置换:

rows = [['Number', 'data1', 'data2'],[2, 40, 30],[3, 40, 25],[4, 50, 30],[5, 30, 10],[6, 25, 5],[7, 50, 10]]
print(list(zip(*rows)))  # 传入二维序列时需要解包
'''执行结果:
[('Number', 2, 3, 4, 5, 6, 7), ('data1', 40, 40, 50, 30, 25, 50), ('data2', 30, 25, 30, 10, 5, 10)]
'''# 注意:该方法会舍弃缺少数据的列(行)
a1 = [1, 2]
a2 = [4, 5, 6]
print(list(zip(a1, a2)))
'''执行结果:
[(1, 4), (2, 5)]
'''

3. 写数据

写入单元格值:

# 写入常规值# 方法1
ws3["A1"] = 1
# 方法2:行号和列号从1开始
ws3.cell(row=2, column=2, value="A2")
# 方法3:追加一行数据(即最下方空白处的最左第一个单元格开始)
ws3.append([1, 2, 3])# 写时间
import time# 方法1
now_time_1 = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
ws3.cell(row=1, column=1, value=now_time_1)# 方法2
import locale
locale.setlocale(locale.LC_ALL, 'en')
locale.setlocale(locale.LC_CTYPE, 'chinese')
now_time_2 = time.strftime("%Y年%m月%d日 %H:%M:%S")  # 自动传入当前时间
ws3.cell(row=1, column=2, value=now_time_2)

合并单元格:

合并单元格以合并区域的左上角的那个单元格为基准,覆盖其他单元格,使之称为一个大的单元格。

相反,拆分单元格后将这个大单元格的值返回到原来的左上角位置。

1 ws3.merge_cells('A1:B1')  # 合并一行中的几个单元格
2 ws3.merge_cells('B2:C11')  # 合并一个矩形区域中的单元格

合并后只可以往左上角写入数据,也就是区间中最左上角的坐标。

如果这些要合并的单元格都有数据,只会保留左上角的数据,其他则丢弃。换句话说若合并前不是在左上角写入数据,那么合并后的单元格则不会有数据。

拆分单元格:

1 ws3.unmerge_cells('A1:B1')  # 拆分后,值回写到A1位置
2 ws3.unmerge_cells('B2:C11')  # 拆分后,值回写到B2位置

4. 设置样式

sheet标签颜色:

1 ws3.sheet_properties.tabColor = "1072BA"

效果:

 行高与列宽:

1 # 第2行行高
2 ws3.row_dimensions[2].height = 40
3 # C列列宽
4 ws3.column_dimensions['C'].width = 30

单元格样式:

'''字体'''
# 等线24号,加粗斜体,字体颜色红色
bold_italic_24_font = Font(name='等线', size=24, italic=True, color=colors.RED, bold=True)
# 直接使用cell的font属性,将Font对象赋值给它
ws3['A1'].font = bold_italic_24_font'''对齐方式'''
# 设置B1中的数据垂直居中和水平居中(除了center,还可以使用right、left等参数)
ws3['B1'].alignment = Alignment(horizontal='center', vertical='center')

6、 yagmail库

使用 yagmail 模块可以更简单地实现邮件发送功能。

安装:

pip install yagmail
import yagmaildef send_mail(report):# 连接邮箱服务器# 注意:若使用QQ邮箱,则password为授权码而非邮箱密码;使用其它邮箱则为邮箱密码yag = yagmail.SMTP(user="****@163.com", password="****", host="smtp.163.com")# 收件人、标题、正文、附件(若多个收件人或多个附件,则可使用列表)yag.send(to="****@163.com", subject="自动化测试报告", contents="请查看附件", attachments=report)# 可简写:yag.send("****@163.com", subject, contents, report)print("email has send out!")if __name__=="__main__":send_mail("e:\\file.txt")

7、pillow 库

安装:

pip install pillow

图片相似度比较:

from PIL import Image
from selenium import webdriverclass ImageCompare(object):'''本类实现了对两张图片通过像素比对的算法,获取文件的像素个数大小,然后使用循环的方式将两张图片的所有项目进行一一对比,并计算比对结果的相似度'''def make_regular_image(self, img, size=(256, 256)):return img.resize(size).convert("RGB")def split_image(self, img, part_size=(64, 64)):# 将图片按给定大小切分w, h = img.sizepw, ph = part_sizeassert w % pw == h % ph == 0return [img.crop((i, j, i + pw, j + ph)).copy() for i in range(0, w, pw) for j in range(0, h, ph)]def hist_similar(self, lh, rh):# 统计切分后每部分图片的相似度频率曲线assert len(lh) == len(rh)return sum(1 - (0 if l == r else float(abs(l - r)) / max(l, r)) for l, r in zip(lh, rh)) / len(lh)def calc_similar(self, li, ri):# 计算两张图片的相似度return sum(self.hist_similar(l.histogram(), r.histogram()) for l, r in zip(self.split_image(li), self.split_image(ri))) / 16.0def calc_similar_by_path(self, lf, rf):li, ri = self.make_regular_image(Image.open(lf)), self.make_regular_image(Image.open(rf))return self.calc_similar(li, ri)if __name__ == "__main__":IC = ImageCompare()driver = webdriver.Chrome()url = "http://www.sogou.com"driver.get(url)driver.save_screenshot("e:\\sogou1.png")driver.get(url)driver.save_screenshot("e:\\sogou2.png")print ("%.2f%%" % (IC.calc_similar_by_path('e:\\sogou1.png','e:\\sogou2.png')*100))  # 99.84%driver.quit()

8、scipy库

图片相似度比较:

from scipy.misc import imread, imsave, imresizea = imread("E:\\sogou1.png")
b = imread("E:\\sogou2.png")# 像素的差异数量
diff = sum((a == b).reshape(-1)) - sum((a != b).reshape(-1))
total = a.shape[0] * a.shape[1] * a.shape[2]
close = diff / totalprint("相似度 = {:.2%}".format(close))

八、Page Object实战

在UI 级的自动化测试框架中,怎样更高效地维护代码,特别是当页面样式改变或者页面元素属性改变,如何做到高效快速地修改代码来适应这些改变了?这个时候就可以考虑使用 Page Objects,也就是页面对象设计模式。在下面知识中结合具体的实例来说明页面对象设计模式在UI自动化测试中的应用。

1、Page Objects的实现

在UI级的自动化测试中,页面对象设计模式表示测试正在交互的Web应用程序用户界面中的一个区域。这减少了代码的重复,也就是说,如果用户界面发生改变,只需要在一个地方修改程序就可以了,使用页面对象设计模式的优点为:

  • 创建可以跨多个测试用例共享的代码;
  • 减少重复代码的数量;
  • 如果用户界面发生变更后,只需要在一个地方维护就可以了。

创建UI,在UI的工程中创建对应的包和目录,如图所示。

注解:在以上工程目录中,在base包里面存放基础代码,在page包里面编写关于页面对象层的代码,若 Web页面发生变更,修改代码主要是在page包中进行。utils包中编写读取文件的方法,testCase包中编写页面对象中所有的测试代码,data文件夹存放数据,测试数据存储在Xml文件中,report存放测试报告。

在base 包中创建 basePage.py 的模块,在该模块中编写基础代码,basePage.py模块的代码如下: 

#!/usr/bin/env python 
#-*-coding:utf-8-*-from selenium import webdriver
from selenium.webdriver.support.expected_conditions import NoSuchElementException
from selenium.webdriver.common.by import By 
import time as tclass WebDriver(object):def__init__(self,driver):self.driver=driverdef findElement(self,*loc):'''单个定位元素的方法'''try:return self.driver.find_element(*loc)except NoSuchElementException as e:print('Error Details (0}'.format(e.args[0])def findsElement(self,*loc):'''多个定位元素的方法'''try:return self.driver.find elements(*loc)except NoSuchElementException as e:print('Error Details (0)'.format(e.args[0])

注解:在以上代码中,定义了类 WebDriver,类的构造函数中 driver指的是webdriver 实例化后对象,在WebDriver 类中编写了单个定位元素的方法和多个定位元素的方法。不管是单个元素定位的方法还是多个元素定位的方法,参数都是*loc,即可以识别元素属性,元素属性主要包含了ID、NAME、CLASS_NAME和TAG_NAME等。

判断自己所写的定位单个和定位多个元素属性的方法是否正确,只需将鼠标放置到 find_element和find_elements 中,按下Ctrl,再按下鼠标左键,如弹出如下信息说明正确,如图所示。

多个元素定位方法如图所示:

在page包中编写对象层的代码,这里以新浪邮箱登录为例,创建 sina.py模块文件,在该模块文件中编写新浪邮箱登录的对象层代码,见代码如下: 

#!/usr/bin/env python 
#-*-coding:utf-8-*-from base.basePage import*
from selenium.webdriver.common.by import Byclass Sina(WebDriver):username_loc=(By.ID,'freename')password_loc=(By.ID,'freepassword')login_loc=(By.LINK_TEXT,'登录')loginError_loc=(By.XPATH,'
/html/body/div[1]/div[1]/div[2]/div/div/div[4]/div[1]/div[1]/div[1]/div[1]/span[1]')def typeUserName(self,username):self.findElement(*self.username_loc).send_keys(username)def typePassword(self,password):self.findElement(*self.password_loc).send_keys(password)@propertydef clickLogin(self):self.findElement(*self.login_loc).click()def login(self,username,password):self.typeUserName(username)self.typePassword(password)self.clickLogin@propertydef getLoginError(self):'''获取错误的信息'''return self.findElement(*self.loginError_loc).text

注解:以上代码定义了Sina的类,该类继承了WebDriver的类。在Sina类中,以类属性的方式指明每个操作的元素属性的值、Id、Name 等,然后依据每个操作步骤编写对应的方法。因为在以上实例中实现的是登录的操作,所以把登录的操作方法封装成一个login的方法,这样实现登录的测试用例直接调用login的方法。getLoginError 返回登录失败的信息,例如,账号和密码为空时点击“登录”按钮返回的错误提示信息。

接下来编写测试层的测试用例代码。在testCase包中创建test_sina.py文件,在该模块文件中编写测试用例,这里以账号和密码为空,以及账号格式不正确为测试点,test_sina.py的代码如下:

#!/usr/bin/env python 
#-*-coding:utf-8-*-import unittest
from page.sina import *
from selenium import webdriver
import time as tclass SinaTest(unittest.TestCase,Sina):def setUp(self):self.driver=webdriver.Firefox()self.driver.maximize_window()self.driver.maximize_window()self.driver.get('https://mail.sina.com.cn/')def tearDown(self):self.driver.quit()def test sinaLogin_001(self):'''登录业务∶账号密码为空验证'''self.login('','')self.assertEqual(self.getLoginError,'请输入邮箱名')def test_sinaLogin_002(self):'''登录业务∶输入不规范邮箱名'''self.login('wuyal303','asd888')self.assertEqual(self.getLoginError,'您输入的邮箱名格式不正确')if __name__ == '__main__':unittest.main(verbosity=2)

注解:在测试模块 test_sina.py的文件中,首先需要导入对象层的类 Sina和unittest单元测试框架。在测试类 SinaTest中,继承了unittest.TestCase和Sina类,继承 TestCase 是由于在编写自动化测试的用例中,用到的测试固件、测试断言和测试执行都需要 TestCase类中的方法,而 Sina类中所包含的对象层的测试操作步骤的方法,继承后可以直接进行调用。测试类SinaTest中编写了两个测试点,在编写测试用例时需要添加备注信息,明确注明该测试用例是测试哪个测试点,验证哪个场景,这样,在测试用例执行失败后,就可以很快知道是哪个业务的哪个功能出现了问题。

在这里把错误的信息和测试地址单独地分离出来,在data目录下创建ui.xml文件,在该文件中编写需要分离的数据,见ui.xml文件的内容:


https://mail.sina.com.cn/#

在utils包中创建operationXml.py的模块,专门用于编写读取Xml文件的内容,operationXml.py模块的代码如下:

#!/usr/hin/env python 
#-*-coding:utf-8-*-import  os
import  xml.dom.minidomclass OperationXml(object):def dir_base(self,fileName,filePath-'data'):'''获取data文件夹下的文件∶param filleName∶要读的文件名称∶param fillePath∶要读的文件名对应的文件夹'''return os.path.join(os.path.dirnane(os.path.dirname(_file_)),filePath,fileName)def getxmlData(self,value):'''获取xml单节点中的数据∶param value∶xml 文件中单节点的名称'''dom=xml.dom.minidom.parse(self.dir base('ui.xml')db = dom.documcntElcmcntname = db.getElementseylagName(valte)nameValue = name[0]return nameValue.firstChild.datadef getXmlUser(self,parent,child):'''获取xml子节点中的数据∶param parent∶xml文件中父节点的名称∶param child∶xml文件中子节点的名称'''dom - xml.dom.minidom.parse(self.dir_base('ui.xml')db = dom.documentElementitemlist - db.getElementsByTagName(parent)item = itemlist[0]return item.getattribute(child)

接下来重构刚才的测试代码。直接从Xml文件中读取Url和错误信息,实现的代码如下:

#!/usr/bin/env python 
#-*-coding:utf-B-*-import unittest 
from page.sina import *
from page.init import *
import  time as t
from utils.operationXml import*class SinaTest(unittest.TestCase,Sina,OperationXml):def setUp(self):self.driver = webdriver.Firefox()self.driver.maximize_window()self.driver.maximize_window()self.driver.get(self.getXmlData('url'))def tearDown(self):self.driver.quit()def test_sinaLogin_001(self,parent='divIext', value='emailNull'):'''登录业务∶账号密码为空验证'''self.login('',')self.assertEqual(self.getLoginError, self.getxmlUser(parent,value))def test sinaLogin 002(self,parent='divIext', value='emailFormat'):'''登录业务∶输入不规范邮箱名        self.login('wuyal303','asd888')self.assertEqual(self.getLoginError, self.getXmlUser(parent,value))i f__name__ == '__main__':unittest.main(verhosity=2)

注解:通过以上方式就可以把测试 URL,返回的错误信息分离到 Xml中,即使某些时候由于某些情况需要修改,只需要在Xml 文件中修改对应的值就可以了。

在介绍 unittest 单元测试框架的时候介绍过,为了后期维护方便,不用重复地编写测试固件,可以把测试固件进行分离,这样后期维护会更方便。例如,在page包中创建init.py文件,把测试固件单独分离出来,init.py的代码如下:

#!/usr/bin/env python 
#-*-coding:utf-8-*-import  unittest
from selenium import webdriver 
from utils.operationXml import*class Init(unittest.TestCase,OperationXml):def setUp(self):self.driver = webdriver.Firefox()self.driver.maximize_window()self.driver.maximize_window()self.driver.get('https://mail.sina.com.cn/')def tearDown(self):self.driver.quit()

分离出测试固件后,那么,测试层中的SinaTest测试类只需要继承 init模块中的Init类就可以了,test_sina.py模块修改后的代码如下:

#!/usr/bin/env python 
#-*-coding:utf-8-*-import unittest 
from page.sina import *
from page.init import *
import time as tclass SinaTest(Init,Sina):def test sinaLogin 001(self,parent='divText',value='emailNull'):'''登录业务∶账号密码为空验证'''self.login('',')self.assertEqual(self.getLoginError, self.getXmlUser(parent,value))def test_sinaLogin_002(self,parent='divText',value='emailFormat'):'''登录业务∶输入不规范邮箱名'''self.login('wuyal303','asd888')self.assertEqual(self.getLoginError, self.getXmlUser(parent,value))i f__name__ == '__main__':unittest.main(verhosity=2)

为了批量执行所有的测试用例,可在allTests.py模块中编写执行所有测试用例及生成测试报告的方法,allTests.py模块的代码如下:

#!/usr/bin/env python 
#-*-coding:utf-8-*-import unittest 
import os
import HTMLTestRunner 
import timedef allTests():'''获取所有需要执行的测试用例'''suite=unittest.defaultTestLoader.discover(
start_dir=cs.path.join (os.path.dirname(_file_),'testCase'), pattern-'test_*·py', top_level_dir=None )return suitedef getNowTime():'''获取当前的时间'''return time.strftime('%Y-%m-%d %H_%M_%S',time.localtime(time.time())def run():fileName=os.path.join(os.path.dirname(_file_), 'report',getNowTime()+'sinaReport.html')fp=cpen(fileName,'wb')runner=HTMLTestRunner.HTMLTestRunner (stream=fp,title='UI自动化测试报告',description='UI自动化测试报告详细信息')runner.run(allTests())if __name__ == '__main__':run()

注解:运行以上代码后,系统就会执行所有的测试用例,执行完成后生成的测试报告存储在report的目录下。

经过以上完善,最新的目录如图所示。

2、Page Objects中引入wait

上面介绍的base包里 basePage.py模块中的基础代码,并不是最佳的方式,在该代码中加入显式等待来解决判断元素是否存在后再进行如点击,输入等操作,在basePage.py 模块的代码中引入 WebDriverWait 类,完善后的代码如下:

#!/usr/bin/env python 
#-*-coding:utf-8-*-from selenium import webdriver
from selenium.webdriver.support.expected_conditions import NoSuchElementException
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.common.by import Byimport time as tclass WebDriver(object):def__init__(self,driver):self.driver=driverdef findElement(self,*loc):'''单个定位元素的方法'''try:return WebDriverWait(self.driver,20).until(lambda
x:x.find_element(*loc))except NoSuchElementException as e:print('Error Details (0}'.format(e.args[0]))def findsElement(self,*loc):'''多个定位元素的方法'''try:return WebDriverWait(self.driver,20).until(lambda
x:x.find_elements(*loc))except NoSuchElementException as e:print('Error Details {0}'.format(e.args[0])

注解:以上代码在原来的基础上增加了显式等待,设置的时间为20 s,即允许系统网络不稳定的情况下最大等待的时间是20 s。lambda是 python的匿名函数,在lambda 函数中,左边的是匿名函数的参数,右边的是匿名函数的表达式。例如,实现两个数相加的函数add,参数是a,b,那么调用add函数赋予实际参数可以实现两个数的相加,使用 lambda实现起来更加简单,lambda实例的代码如图所示。

再次执行testCase模块下的test_sina.py中测试类中的测试代码,验证本次修改基础代码是否对测试用例有影响,运行代码后测试用例全部通过。

3、Page Objects引入工厂设计模式

在UI级别的自动化测试中,Selenium主要应用在Web应用程序的自动化测试中,Appium应用在App应用程序的自动化测试中,在Appium的自动化测试框架中,元素定位的类继承了Selenium 代码中的By 类,mobileby 模块下的MobileBy类的代码如下: 

from selenium.webdriver.common.by import Byclass MobileBy(By):IOS PREDICATE = '-ios predicate string'IOS UIAUTOMATION = '-ios uiautomation'    IOS CLASS CHAIN = '-ios class chain'ANDROID UIAUTOMATOR = '-android uiautomator'ACCESSIBILITY_ID='accessibility id'IMAGE = '-image'

在以上代码中可以看到,MobileBy类继承了By类,也就是说在App的应用自动化测试中,元素定位也是支持元素属性ID等的。

对 base模块下的basePage.py模块代码再次进行扩展,让元素定位部分的方法同时支持对 Web应用程序产品的元素定位,也支持对 App应用程序的元素定位。在这里引入工厂方法模式,在工厂方法模式中,工厂方法用于创建产品,并隐藏了产品对象实例化的过程。对 basePage.py 模块的代码再次进行重构,定义Factory类创建不同的测试对象,basePage.py模块的代码如下:

 

#!/usr/bin/env python 
#-*-coding:utf-8-*-from selenium import webdriver
from selenium.webdriver.support.expected conditions import NoSuchElementException
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.common.by import By
from appitm.webdriver.common.robileby import MobileBy 
import time as tclass Factory(object):def __init__(self,driver):self.driver=driver#工厂方法def createDriver(self, driver):if driver=='web':return WebUI(self.driver)elif driver=='app':return AppUI(self.driver)class WebDriver(object):def __init__(selE,driver):self.criver-driverdef findElement(self,*loc):'''单个定位元素的方法'''try:return WebDriverWait(self.driver,20).until(lambda
x:x.tind_element(*loc))except NoSuchElementExcention as e:print('Ezror Details (0)'.format(e.args[0]))def findsElement(self,*loc):'''多个定位元素的方法'''try:return WebDriverWait(self.criver,20).until(lambda
x:x.find elements(*loc))except NoSuchElementException as e:print('Ezror Details (0]'.format(e.args[C]>)class WebUI(WebDriver):def __str__(self):return 'WebUI'class AppUI(WebDriver):def __str__(self):return 'AppUI'

注解:在上面的代码中,在Factory 类中定义了工厂类,Factory 类生成WebDriver 对象。定义 Factory 类创建不同的WebDriver 对象。WebUI 类和AppUI类继承自 WebDriver类,WebUI和AppUI可以看作是具体的测试对象产品(Web和App)。在Factory类中定义了工厂方法createDriver,工具字符串类型 driver的值,生成不同的WebDriver对象。如果 driver对象是“web”,则调用WebUI,返回WebUI类的实例。如果driver对象是“app”,则调用AppUI,返回AppUI类的实例。

重构基础代码后,在对象层的代码中,如果是基于Web 应用程序的类就继承WebUI,如果是基于App应用程序的类就继承AppUI,在前面的page模块下的sina.py模块中,对象层Sina类由原来继承WebDriver修改为继承WebUI,再次执行testCase模块下test_sina.py模块中的测试代码,可以正常地执行通过。

在以上实例中,引入了工厂设计模式,这里以微博的App 作为测试实例来说明该模式在UI自动化测试中的应用,不管是针对 WEB还是 App。在对象层的包中创建 weibo.py的模块,在该模块中创建 WeiBo类,该类继承 AppUI,在类WeiBo中编写微博获取手机验证码的方法,weibo.py模块的代码如下:

#!/usr/bin/env python 
#-*-coding:utf-8-*-from selenium import webdriver
from selenium.webdriver.common.by import By 
from base.basePage import *
from time import sleep 
from appium import webdriverclass WeiBo(AppUI):login_loc=(By.XPATH,"//android.widget.TextView[@text='登录']")phone_loc=(By.XPATH,"//android.widget.EditText[@text='输入手机号']")    codeButton_loc=(By.XPATH,"//android.widget.Button[@text='获取验证码']")@propertydef clickMy(self):sleep(3)self.findElement(*self.login_loc).click()def typePhone(self,phone):self.findElement(*self.phone_loc).send_keys(phone)def clickCodeButton(self):self.findElement(*self.codeButton_loc).click()def getPhoneCode(self,phone):self.clickMyself.typePhone(phone)self.clickCodeButton()

注解:以上代码中,类 WeiBo 继承了AppUI,在该类中首先获取元素的属性,接下来依次根据元素的属性编写对应的方法,最后把获取手机验证码的操作步骤封装成一个方法getPhoneCode。

在测试包 testCase 中创建 test_weibo,编写对应的测试用例。在执行测试用例前,首先保证 Appium 的环境搭建是正确的,在cmd 命令提示符下输入appium-doctor,会自动检测Appium的环境是否正确,显示如图所示。

编写测试代码,test_weibo.py模块的代码如下:

#!/usr/bin/env python #-*-coding:utf-8-*-import unittest 
import time as t
from appium import webdriver
from page.weibo import WeiBoclass WeiBoTest(unittest.TestCase,WeiBo):def setUp(self):desired_caps={}desired_caps['platformName']='Android'desired_caps['platformVersion']='6.0'desired_caps['deviceName']='meizu_M5'desired_caps['appPackage']='com.sina.weibo'desired_caps['appActivity']='com.sina.weibo.SplashActivity'desired_caps['unicodeKeyboard']=True desired_caps['resetKeyboard']=Trueself.driver=webdriver.Remote('http://127.0.0.1:4723/wd/hub', desired_caps)def test_001(self):t.sleep(3)self.getPhoneCode('13456787654')def tearDown(self):self.driver.quit()if __name__ == '__main__':unittest.main(verbosity=2)

注解:在测试模块 test_weibo.py 中首先需要导入 untitest 单元测试框架、appium 包中对应的webdriver 类,以及对象层的代码 WeiBo 类。在测试类WeiBoTest 中继承了TestCase和WeiBo 类之后,接着在该测试类中编写获取手机验证码的测试用例。

接下来启动Appium的服务,启动后的Appium服务如图所示:

使用数据线在电脑上连接手机,并将手机设置为开发者模式,连接成功后,在电脑cmd命令提示符下输入adb devices -l终端显示如下的信息,表示手机连接成功,如图所示。 

运行test_weibo.py测试模块中的代码,测试可以正常执行。在移动产品的测试中,手机 app 启动后需要左右滑动才可以看到登录的页面,在base 包中编写的swipe.py模块中对Swipe类进行了二次封装,封装的代码如下:

#!usr/bin/env python 
#!coding:utf-8from appium import webdriver 
import time as tclass Swipe(object):def__init__(self,driver):self.driver=driver@property def width(self):return self.driver.get_window_size()['width']@propertydef height(self):return self.driver.get_window_size()['height']@propertydef getResolution(self):return str(self.width)+"*"+str(self.height)@propertydef set_Left_Right(self):'''∶return∶实现从左到右滑动,滑动时x轴起点大于终点'''t.sleep(2)self.driver.swipe(self.width*9/10,self.height/2, self.width/20,self.height/2,0)@propertydef set_Right_Left(self):'''∶return∶实现从右到左滑动,滑动时x轴起点小于终点'''t.sleep(2)self.driver.swipe(self.width/10,self.height/2, self.width*9/10,self.height/2,0)@propertydef set_Up_Down(self):'''∶return∶实现从上往下滑动,滑动时Y轴起点大于终点111'''t.sleep(2)self.driver.swipe(self.width/2,self.height*9/10, self.width/2,self.height/20,0)@propertydef set_Down_Up(self):'''∶return∶实现从下往上滑动,滑动时Y轴起点小于终点11'''t.sleep(2)self.driver.swipe(self.width/2,self.height/20, self.width/2,self.height*9/10,0)

注解:Swipe 类的构造函数是 driver,实际上对 Swipe 类实例化的时候,它的参数就是 webdriver实例化后的对象,例如,对 Swipe类实例化,实例代码为per=Swipe(self.driver)。接下来依次获取手机的高度和宽度,然后不管是上下滑动还是左右滑动都是根据手机坐标来进行设置和操作的。

结合以上实例,可以看到在框架中引入工厂设计模式后,对代码进行了重构和封装。框架既可以对 Web应用产品做 UI自动化测试,也可以对 App产品做UI自动化测试。

九、UI自动化测试实战

1、Web产品的实战

结合一个具体的产品,测试在一个用户登录系统后,创建一个用户,然后查询该用户,最后删除该用户。在第六章代码的基础上,在对象层的包中创建login的模块文件,在该模块中编写登录方法,实现的代码如下:

#!/usr/bin/env python 
#-*-coding:utf-8-*-from base.basePage import*
from selenium.webdriver.common.by import Byclass Login(WebUI):username_loc=(By.NAME,'username')password_loc=(By.NAME,'password')login_loc=(By.CSS_SELECTOR,'/*[@id="app"]/div/div/div[2]/form/div[4]/button')def typeUserName(self,username):self.findElement(*self.username_loc).send_keys(username)def typePassword(self,password):self.findElement(self.password_loc).send_keys (password)@propertydef clickLogin(self):self.findElement(*self.login_loc).click()def login(self,username='66**66',passwd='asd***'):self.typeUserName(username)self.typePassword(passwd)self.clickLogin

接下来创建 user.py模块的代码,在user.py模块中编写添加用户、查询用户和删除用户对象层的代码,代码如下:

#!/usr/bin/env python 
#-*-coding:utf-8-*-from base.basePage import *
from selenium.webdriver.common.by import Byclass User(WebUI):#添加用户元素属性adcUser_loc=(By.LINK_TEXT,'添加商户')#账号输入框元素属性account_loc=(By.XPATH,'//*[@id="app"]/div/div[1]/div/div[2]/div/div/div[1]/div/irput')#用户姓名输入框元素属性name_loc=(By.XPATH,'//*[@id="app"]/div/div[1]/div/div[2]/div/div/div/div[2]/div/input')#账号密码输入框元素属性passwd_loc=(By.XPATH,'//*[@id="app"]/div/div[1]/div/div[2]/div /div/div[3]/div/input')#添加用户保存按钮元素属性save_loc=(By.XPATH,'//*[@id="app"]/div/div[1]/div/div[2]/div/div/div[10]/div/button')@propertydef clickAddUser(self):'''点击添加商户按钮'''self.findElement(*self.addUser_loc).click()def typeAccount(self,account):self.findElement(*self.account_loc).send_keys(account)def typeName(self, name):self.findElement(*self.name_loc).send_keys(name)def getUserName(self):'''获取用户输入框填写的用户名称'''return self.findElement(*self.name_loc).get_attribute('value')def typePasswd(self,passwd):self.findElement(*self.passwd_loc).send keys(passwd)@propertydef clickSave(self):self.findElement(*self.save_loc).click()def addUer(self,account='666666',name='无涯',passwd='123456')∶'''创建用户'''self.clickAddUserself.typeAccount(account)self.typeName(name)name=self.getUserName()self.typePasswd(passwd)self.clickSave self.clickUserManage return name'''用户列表'''#导航栏用户管理元素属性userManage_loc=(By.LINK_TEXT,'商户管理')#用户列表中用户名称元素属性userName_loc=(By.LINK_TEXT,'无涯')#用户管理中用户查询输入框元素属性userSo_loc=(By.XPATH,'//*[@id-"app"]/div/div[1]/div[1]/div/div[2]/input')#查询按钮元素属性userQuery_loc=(By.XPATH,'//*[@id="app"]/div/div[1]/div[1]/div /div[3]/button')#用户列表下拉框操作元素属性userSel_loc=(By.XPATH,'//*[@id="app"]/div/div[1]/div[2]/div[2]/div/table/tbody/tr/td[6]/div/i')#删除用户元素属性userDel_loc=(By.XPATH,'/*[@id="app"]/div/div[1]/div[2]/div[2]/div/table/tbody/tr/td[6]/div/ul/li[2]')#删除用户确定弹出框元素属性userDelok_loc=(By.XPATH,'/html/body/div[5]/div[2]/div/div/div /div/div[2]/button[1]')#用户列表无数据时的元素属性listNo_loc=(By.XPATH,'//*[@id="app"]/div/div[1]/div[2]/div[2]/div/table/tbody/tr/td/div')@propertydef clickUserManage(self):'''点击用户管理'''t.sleep(3)self.findElement(*self.userManage_loc).click()@propertydef getListUserName(seli):'''获取用户列表中的用户名称'''return self.findElement(*self.userName loc).textdef typeUserSo(self,name-'无涯')∶self.findElement(*self.use:So_loc),send_keys(name)@propertydef clickQuery(self):self.findRlement(*self.userQuery_loc).click0@propertydef clickUserSel(self):'''点击用户列表操作下拉框'''self.findElement('self.userSel_loc).click()@propertydef clickUserDel(self):'''点击删除用户按钮'''self.findElement(*self.userDel_loc).click()self.clickUserDelok@propertydef clickUserDelok(self)∶'''点击遗除用户弹出框确定按钮'''t.sleep(2)self.findElement(*self.userDelok_loc).click()@propertydef userDel(self):'''除用户'''self.clickuserSel self.click0serDel t.sleep(3)@propertydef getListNo(self):'''获取用户列表查询无数据时的提示信息'''return self.findElement(*self.listNo_loc).textdef isAddUser(self):'''创建用户增加判断,如用户存在,先删除再创建如不存在,就直接创建用户'''try:self.typeUserSo()self.clickQueryassert self.getListUserName in'无涯't.sleep(3)self.userDelexcept:return self.addUer()else:return self.addUer()finally:pass

注解:在User 类中,编写了添加用户、用户列表、用户查询及删除用户的元素属性和单独的方法。在添加 addUser用户的方法中,添加用户成功后返回添加用户时填写的用户名称,这样做的目的是在添加用户成功后,获取用户列表的用户名称,与添加用户时填写的用户名称进行断言,如果一致,表示添加用户成功,不一致表示添加用户存在问题。在添加用户的时候,获取用户名输入框填写的名称方法为getUserName,此方法使用了get_attribute(value)获取输入框填写的内容。

在添加用户时存在的问题是添加的用户已存在,从而导致添加用户失败,这并不能说明功能存在问题,而是在对象层中需要处理这类逻辑情况。在添加用户的时候,对添加的用户进行查询,如果用户存在(可能是手工测试添加的或者是上一次执行自动化测试代码添加的),则删除用户再次创建;如果不存在则创建用户。在对象层中该方法为 isAddUser,这样执行创建用户的测试用例,不管以后用户是否存在,代码都能够执行下去,如果不这样处理,可能因为对象层的代码没进行逻辑处理而导致代码执行失败,也就无法判断到底是添加用户的功能存在问题还是代码自身有问题。

在对象层的init.py模块中,在分离出的测试固件中将测试地址从新浪邮箱修改为http://118.***.***.145:9098/#/login,修改后的代码如下:

#!/usr/bin/env python 
#-*-coding:utf-8-*-import  unittest
from selenium import webdriverclass Init(unittest.TestCase):def setUp(self):self.driver = webdriver.Firefox()self.driver.maximize_window()self.driver.maximize_window()self.driver.get('http://118.***.***.145:999/#/login')def tearDown(self):self.driver.quit()

编写完对象层的代码后,在testCase模块下创建test_user.py的模块编写创建用户,查询用户和删除用户的测试用例,test_user.py的代码如下:

#!/usr/bin/env python 
#-*-coding:utf-8-*-import unittest 
from page.user import *
from page.init import *
from page.login import*class UserTest(Init,User):def login(self):'''登录系统的方法'''ecp=Login(self.driver)ecp.login()def test_user_add(self):'''用户管理业务∶创建用户'''self.login()name=self.isAddUser()listName=self.getListUserName self.userDelself.assertEqual(listName,name)def test_user_query(self):'''用户管理业务∶查询用户'''self.login()name = self.isAddUser()#依据用户名查询用户self.typeUserSo()self.clickQuerylistName=self.getListUserName self.userDelself.assertEqual(listName,name)def test_user_delete(self):'''用户管理业务∶删除用户'''self.login()#创建用户self.isAddUser()#删除用户self.userDel#依据用户名查询用户是否存在self.typeUserSo()self.clickQueryself.assertEqual(self.getListNo,'没有记录')if __name__ == '__main__':unittest.main(verbosity=2)

注解:在UserTest类中,继承了Init类和User类,在Init类中编写了测试固件及需要测试的地址,而 User类就是对象层的代码。在对象层 User类中编写了添加用户、查询用户和删除用户的方法;在UserTest类中编写了添加用户、查询用户和删除用户的测试用例。由于在UserTest测试类中,不管是添加用户、查询用户还是删除用户,都需要登录到系统中,所以,可以把登录方法分离出来,单独写一个login方法。在login方法中,对 Login实例化后调用了Login类中的login方法。这样,在添加用户、查询用户及删除用户时都可直接调用 login方法登录到系统。在测试类UserTest中,添加用户,查询用户,删除用户的测试用例之间是没有任何依赖关系的,这样测试用例执行的时候就不会相互影响。

不管是查询用户还是删除用户,首先都需要添加用户,每个测试用例执行完成后,之前添加的数据都会被清空,这样,在下次执行的时候就不会有影响。在添加用户测试用例,也就是test_user_add的测试用例中,添加用户成功后,先获取用户填写的用户名称,最后获取添加用户成功后用户列表中显示的用户名称,再删除用户,最后对添加的测试用例进行断言。

这里特别需要注意的是,断言务必放在删除用户之后,不管添加用户这个测试用例是否执行成功,添加用户后,把这条数据删除,再来验证添加用户填写的用户名称和用户列表中显示的用户名称。

如果断言在删除用户之前,有可能断言失败,导致删除用户的代码无法执行,对下一次执行测试用例产生影响。

在init.py模块中的Init类中,测试地址并没有分离出去,这里把测试中用到的数据分离到xml文件中,在data文件夹中创建ui.xml文件,内容如下:


http://118.***.***.145:9999/#/login

在utils模块中创建helper.py模块,在helper.py模块中实现读取Xml文件中的内容,helper.py的代码如下:

#!/usr/bin/env python 
#-*-coding:utf-8-*-import os
import xml.dom.minidomclass Helper(object):def dir_base(self,fileName,filePath='data'):'''获取data文件夹下的文件∶param fileName∶要读的文件名称∶param filePath∶要读的文件名对应的文件夹'''return os.path.join(os.path.dirname(os.path.dirname(_file_)),filePath,fileName)def getXmlData(self,value):'''获取xml单节点中的数据∶param value∶xml文件中单节点的名称'''dom = xml.dom.minidom.parse(self.dir_base('ui.xml')db = dom.documentElementname = db.getElementsByTagName(value)nameValue = name[0]return nameValue.firstChild.datadef getXmlUser(self,parent,child):'''获取xml子节点中的数据∶param parent∶xml文件中父节点的名称∶param child∶xml文件中子节点的名称'''dom = xml.dom.minidom.parse(self.dir base('ui.xml')db = dom.documentElementitemlist = db.getElementsByTagName(parent)item = itemlist[0]return item.getAttribute(child)

注解:在Helper类中的dir_base方法用来获取需要读取文件夹下文件的路径,方法getXmlData用来读取Xml文件中单节点中的数据,方法getXmlUser用来读取父节点下子节点中的数据。

utils包中的helper模块中的Helper类已实现读取 Xml文件中的内容,这里把init.py模块中的Init类的代码进行重构,直接读取Xml文件中测试的地址,修改后的代码如下:

#!/usr/bin/env python 
#-*-coding:utf-8-*-import unittest
from selenium import webdriver 
from utils.helper import *class Init(unittest.TestCase,Helper):def setUp(self):self.driver = webdriver.Firefox()self.driver.maximize_window()self.driver.maximize window()self.driver.get(self.getXmlData('url')def tearDown(self):self.driver.quit()

注解:在init模块中先导入Helper类,使Init类继承Helper类。加载测试地址直接调用Xml文件中的测试地址。

在测试类UserTest删除用户的测试用例中,把提示信息“没有记录”分离到Xml并且读取它,该测试类的代码如下:

def test_user_delete(self,parent='data',child='notData'):'''用户管理业务∶删除用户'''self.login()#创建用户self.isAddUser()#删除用户self.userDel#依据用户名查询用户是否存在self.typeUserSo()self.clickQueryself.assertEqual(self.getListNo,self.getXmlUser(parent,child))

注解:在删除用户的测试用例代码中,把提示信息分离到 Xml 文件中,这样,在测试用例中就不会存在提示信息了,因为提示信息在多个地方会使用到,如需修改时,我们只需要在Xml 文件中进行修改,而不需要在多个测试用例中应用到的地方逐一修改。

运行 allTests.py模块的文件。测试执行成功后,会在report文件夹下生成测试报告,生成的测试报告如图所示。

打开report文件夹下的测试报告,如图所示。 

在Jenkins创建Job,Job名称为UI,在jenkins执行UI工程下的测试用例,创建后的Job视图如图所示。 

选择“立即构建”选项后,执行测试用例,执行后的测试报告如图所示。 

至此,一个完整的UI级的自动化测试代码更新完毕。

在UI级的自动化测试中,最大的困难在于维护成本的增加,所以一定要将测试中的公共数据分离出来,并对用到的方法依据业务逻辑进行封装。

补充:

在Windows 的cmd 命令提示符中,pip3查询库或者安装库,都会忽略库的大小写。而在Python 的正式编辑代码中,库则严格遵循大小写。在Python 中,模块的名称一般都是小写,每一个Python文件就是一个模块,但是类的定义,首字母必须是大写。 

相关内容

热门资讯

AWSECS:访问外部网络时出... 如果您在AWS ECS中部署了应用程序,并且该应用程序需要访问外部网络,但是无法正常访问,可能是因为...
【NI Multisim 14...   目录 序言 一、工具栏 🍊1.“标准”工具栏 🍊 2.视图工具...
银河麒麟V10SP1高级服务器... 银河麒麟高级服务器操作系统简介: 银河麒麟高级服务器操作系统V10是针对企业级关键业务...
AWSElasticBeans... 在Dockerfile中手动配置nginx反向代理。例如,在Dockerfile中添加以下代码:FR...
不能访问光猫的的管理页面 光猫是现代家庭宽带网络的重要组成部分,它可以提供高速稳定的网络连接。但是,有时候我们会遇到不能访问光...
月入8000+的steam搬砖... 大家好,我是阿阳 今天要给大家介绍的是 steam 游戏搬砖项目,目前...
​ToDesk 远程工具安装及... 目录 前言 ToDesk 优势 ToDesk 下载安装 ToDesk 功能展示 文件传输 设备链接 ...
北信源内网安全管理卸载 北信源内网安全管理是一款网络安全管理软件,主要用于保护内网安全。在日常使用过程中,卸载该软件是一种常...
AWS管理控制台菜单和权限 要在AWS管理控制台中创建菜单和权限,您可以使用AWS Identity and Access Ma...
AWR报告解读 WORKLOAD REPOSITORY PDB report (PDB snapshots) AW...