06-Page Object+pytest单元测试框架

Page Object


Page  Object 是  UI 自动化测试项目开发实践的最佳设计模式之一,它的主要特点体现
在对界面交互细节的封装上,使测试用例更专注于业务的操作,从而提高测试用例的可维
护性。

8.1    认识 Page Object

当为 Web 页面编写测试时,需要操作该 Web 页面上的元素。然而,如果在测试代码中
直接操作 Web 页面上的元素,那么这样的代码是极其脆弱的,因为 UI 会经常变动。
Page Object 原理如图 8-1 所示。
page 对象的一个基本经验法则是:凡是人能做的事,page 对象通过软件客户端都能做
到。因此,它应当提供一个易于编程的接口,并隐藏窗口中底层的部件。当访问一个文本
框时,应该通过一个访问方法(Accessor  Method)实现字符串的获取与返回,复选框应当
使用布尔值,按钮应当被表示为行为导向的方法名。page 对象应当把在 GUI 控件上所有查
询和操作数据的行为封装为方法。

一个好的经验法则是,即使改变具体的元素,page 对象的接口也不应当发生变化。
尽管该术语是 page 对象,但并不意味着需要针对每个页面建立一个这样的对象。例如,
页面上有重要意义的元素可以独立为一个 page 对象。经验法则的目的是通过给页面建模,
使其对应用程序的使用者变得有意义。

Page Object 是一种设计模式,在自动化测试开发中应遵循这种设计模式来编写代码。
Page Object 应该遵循以下原则进行开发:
	Page Object 应该易于使用。
	有清晰的结构,如 PageObjects  对应页面对象,PageModules 对应页面内容。
	只写测试内容,不写基础内容。
	在可能的情况下防止样板代码。
	不需要自己管理浏览器。
	在运行时选择浏览器,而不是类级别。
	不需要直接接触 Selenium。

8.2 实现 Paget Object

下面我们将通过例子介绍这种设计模式的使用。

8.2.1  Paget Object 简单实例

以百度搜索为列,假设我们有如下测试代码。
…
def  test_baidu_search_case1(self):
self.driver.get(self.base_url)
self.driver.find_element_by_id("kw").send_keys("selenium")
self.driver.find_element_by_id("su").click()

def  test_baidu_search_case2(self):
self.driver.get(self.base_url)
self.driver.find_element_by_id("kw").send_keys("unittest")
self.driver.find_element_by_id("su").click()

def  test_baidu_search_case3(self):
self.driver.get(self.base_url)
self.driver.find_element_by_id("kw").send_keys("page  object")
self.driver.find_element_by_id("su").click()
这段代码最大的问题就是在三条测试用例中重复使用了元素的定位和操作。这会带来
一个很大的问题,当元素的定位发生变化后,例如,id=kw 失效了,应及时调整定位方法,
这时就需要在三条测试用例当中分别进行修改。假设,我们的自动化项目有几百条测试用
例,而 UI 很可能是频繁变化的,那么就会提高自动化测试用例的维护成本。
Page Object 的设计思想上是把元素定位与元素操作进行分层,这样带的来最直接的好
处就是当元素发生变化时,只需维护 page 层的元素定位,而不需要关心在哪些测试用例当
中使用了这些元素。在编写测试用例时,也不需要关心元素是如何定位的。

创建 baidu_page.py 文件,内容如下。
class  BaiduPage():

def  __init__(self,  driver):
    self.driver  =  driver

def  search_input(self,  search_key):
    self.driver.find_element_by_id("kw").send_keys(search_key)

def  search_button(self):


    self.driver.find_element_by_id("su").click()
首先,创建 BaiduPage 类,在__init__()初始化方法中接收参数 driver 并赋值给 self.driver。
然后,分别封装 search_input()方法和 search_button()方法,定位并操作元素。这里的封装只
针对一个页面中可能会操作到的元素,原则上是一个元素封装成一个方法。当元素的定位
方法发生改变时,只需维护这里的方法即可,而不需要关心这个方法被哪些测试用例使用
了。
from  baidu_page  import  BaiduPage
…

def  test_baidu_search_case1(self):
self.driver.get(self.base_url)
bd  =  BaiduPage(self.driver)
bd.search_input("selenium")
bd.search_button()

def  test_baidu_search_case2(self):
self.driver.get(self.base_url)
bd  =  BaiduPage(self.driver)
bd.search_input("unittest")
bd.search_button()

def  test_baidu_search_case3(self):
self.driver.get(self.base_url)
bd  =  BaiduPage(self.driver)
bd.search_input("page  object")
bd.search_button()
…
首先在测试中导入  BaiduPage  类,然后在每个测试用例中为  BaiduPage  类传入驱动,
这样就可以轻松地使用它封装的方法来设计具体的测试用例了。这样做的目的就是在测试
用例中消除元素定位。如果你要操作百度输入框,那么只需调用  search_input()方法并传入
搜索关键字即可,并不需要关心百度输入框是如何定位的。

8.2.2  改进 Paget Object 封装

上面的例子演示了 Page Object 设计模式的基本原理,这样的分层确实带来了不少好处,
但同时也带来了一些问题。例如,需要写更多的代码。以前一条测试用例只需写 4 到 5 行
代码即可,现在却不得不先在 Page 层针对每个待操作的元素进行封装,然后再到具体的测试用例中引用。为了使 Page 层的封装更加方便,我们做一些改进。
创建 base.py 文件,内容如下。
import  time

class  BasePage:
"""
基础 Page 层,封装一些常用方法
"""

def  __init__(self,  driver):
self.driver  =  driver

#  打开页面
def  open(self,  url=None):
if  url  is  None:
self.driver.get(self.url)
else:
self.driver.get(url)

#  id  定位
def  by_id(self,  id_):
return  self.driver.find_element_by_id(id_)

#  name  定位
def  by_name(self,  name):
return  self.driver.find_element_by_name(name)

#  class  定位
def  by_class(self,  class_name):
return  self.driver.find_element_by_class_name(class_name)

#  XPath  定位
def  by_xpath(self,  xpath):
return  self.driver.find_element_by_xpath(xpath)

#  CSS  定位
def  by_css(self,  css):
return  self.driver.find_element_by_css_selector(css)

#  获取 title
def  get_title(self):




return  self.driver.title

#  获取页面 text,仅使用 XPath 定位
def  get_text(self,  xpath):
return  self.by_xpath(xpath).text

#  执行 JavaScript 脚本
def  js(self,  script):
self.driver.execute_script(script)
创建 BasePage 类作为所有 Page 类的基类,在 BasePage 类中封装一些方法,这些方法
是我们在做自动化时经常用到的。
 open()方法用于打开网页,它接收一个  url  参数,默认为  None。如果  url  参数为 None,则默认打开子类中定义的 url。稍后会在子类中定义 url 变量。  by_id()和  by_name()方法。我们知道,Selenium  提供的元素定位方法很长,这里 做了简化,只是为了在子类中使用更加简便。  get_title()和 get_text()方法。这些方法是在写自动化测试时经常用到的方法,也可 以定义在 BasePage 类中。需要注意的是,get_text()方法需要接收元素定位,这里默认为 XPath 定位。 当然,我们还可以根据自己的需求封装更多的方法到 BasePage 类中。

修改 baidu_page.py 文件。

from  base  import  BasePage

class  BaiduPage(BasePage):
"""百度 Page 层,百度页面封装操作到的元素"""
url  =  "https://www.baidu.com"

def  search_input(self,  search_key):
self.by_id("kw").send_keys(search_key)

def  search_button(self):
self.by_id("su").click()
创建 BaiduPage.py 类继承 BasePage 类,定义 url 变量,供父类中的 open()方法使用。
这里可能会有点绕,所以举个例子:小明的父亲有一辆电动玩具汽车,电动玩具汽车需要
电池才能跑起来,但小明的父亲并没有为电动玩具汽车安装电池。小明继承了父亲的这辆
电动玩具汽车,为了让电动玩具汽车跑起来,小明购买了电池。在这个例子中,open()方法


就是“电动玩具汽车”,open()方法中使用的 self.url 就是“电池”,子类中定义的 url 是为
了给父类中的 open()方法使用的。
在 search_input()和 search_button()方法中使用了父类的 self.by_id()方法来定位元素,比
原生的 Selenium 方法简短了不少。
在测试用例中,使用 BaiduPage 类及它所继承的父类中的方法。
import unittest
from time import sleep
from selenium import webdriver
from baidu_page import BaiduPage


class TestBaidu(unittest.TestCase):

    @classmethod
    def setUpClass(cls):
        cls.driver = webdriver.Chrome()

    def test_baidu_search_case1(self):
        page = BaiduPage(self.driver)
        page.open()
        page.search_input("selenium")
        page.search_button()
        sleep(2)
        self.assertEqual(page.get_title(), "selenium_百度搜索")

    @classmethod
    def tearDownClass(cls):
        cls.driver.quit()


if __name__ == '__main__':
    unittest.main(verbosity=2)
因为前面封装了元素的定位,所以在编写测试用例时会方便不少,当需要用到哪个 Page
类时,只需将它传入浏览器驱动,就可以使用该类中提供的方法了。

8.3    poium 测试库

poium 是一个基于 Selenium/appium 的 Page  Object 测试库,最大的特点是简化了 Page层元素的定义。

项目地址:https://github.com/defnngj/poium。
支持 pip 安装。

>  pip  install  poium

8.3.1  基本使用

使用 poium 重写 baidu_page.py。
from  poium  import  Page,  PageElement

class  BaiduPage(Page):
"""百度 Page 层,百度页面封装操作到的元素"""
search_input  =  PageElement(id_="kw")
search_button  =  PageElement(id_="su")
创建 BaiduPage 类,使其继承 poium 库中的 Page 类。调用 PageElement 类定义元素定
位,并赋值给变量 search_input 和 search_button。这里仅封装元素的定位,并返回元素对象,
元素的具体操作仍然在测试用例中完成,这也更加符合 Page Object 的思想,将元素定位与
元素操作分层。

在测试用例中的使用如下。
from  baidu_page  import  BaiduPage

class  TestBaidu(unittest.TestCase):

…
def  test_baidu_search_case1(self):
page  =  BaiduPage(self.driver)
page.get("https://www.baidu.com")
page.search_input  =  "selenium"
page.search_button.click()

…
首先导入 BiaduPage 类,传入浏览器驱动。然后,调用 get()方法访问 URL,该方法由
Page 类提供。接下来调用 BaiduPage 类中定义的元素对象,即 search_input 和 search_button  ,
实现相应的输入和单击操作。


8.3.2  更多用法

想要更好地使用 poium,需要了解下面的一些使用技巧。

1.支持的定位方法

poium 支持 8 种定位方式。
from  poium  import  Page,  PageElement

class  SomePage(Page):
elem_id  =  PageElement(id_='id')
elem_name  =  PageElement(name='name')
elem_class  =  PageElement(class_name='class')
elem_tag  =  PageElement(tag='input')
elem_link_text  =  PageElement(link_text='this_is_link')
elem_partial_link_text  =  PageElement(partial_link_text='is_link')
elem_xpath  =  PageElement(xpath='//*[@id="kk"]')
elem_css  =  PageElement(css='#id')
2.设置元素超时时间

通过 timeout 参数可设置元素超时时间,默认为 10s。
from  poium  import  Page,  PageElement

class  BaiduPage(Page):
search_input  =  PageElement(id_='kw',  timeout=5)
search_button  =  PageElement(id_='su',  timeout=30)
3.设置元素描述

当一个  Page  类中定义的元素非常多时,必须通过注释来增加可读性,这时可以使用
describe 参数。
from  poium  import  Page,  PageElement

class  LoginPage(Page):
"""
登录 Page 类
"""
username  =  PageElement(css='#loginAccount',  describe="用户名")
password  =  PageElement(css='#loginPwd',  describe="密码")



login_button  =  PageElement(css='#login_btn',  describe="登录按钮")
user_info  =  PageElement(css="a.nav_user_name  >  span",  describe="用户信息")
需要强调的是,describe 参数并无实际意义,只是增加了元素定义的可读性。

4.定位一组元素

当我们要定位一组元素时,可以使用 PageElements 类。
from  poium  import  Page,  PageElement

class  ResultPage(Page):
#  定位一组元素
search_result  =  PageElements(xpath="//div/h3/a")
poium 极大地简化了 Page 层的定义,除此之外,它还提供了很多的 API,如 PageSelect
类简化了下拉框的处理等。读者可以到 GitHub 项目中查看相关信息。目前,poium 已经在
Web 自动化项目中得到了很好的应用。
第 9 章
pytest单元测试框架

在学习了  unittest  单元测试框架之后,还有必要学习  pytest  吗?答案是肯定的。pytest
是一个第三方单元测试框架,更加简单、灵活,而且提供了更加丰富的扩展,弥补了 unittest
在做 Web 自动化测试时的一些不足。

9.1    pytest 简单例子

pytest 官方网站:https://docs.pytest.org/en/latest/。
pytest 支持 pip 安装。

>  pip  install  pytest

通过 pytest 编写一个简单的测试用例,创建 test_sample.py 文件。

def  inc(x):
return  x  +  1

def  test_answer():
assert  inc(3)  ==  5
这是官方给出的一个例子。inc()函数接收一个参数  x,返回  x+1。test_answer()为测试
用例,调用 inc()方法并传参数为 3,使用 assert 断言返回结果是否为 5。
接下来运行测试,切换到 test_sample.py 文件所在目录,执行“pytest”命令。
>  pytest
=================  test  session  starts  ==================
platform  win32  --  Python  3.7.1,  pytest-4.3.0,  py-1.8.0,  pluggy-0.9.0

rootdir:  D:\git\book-code\pytest_sample\first_demo,  inifile:
collected  1  item

test_sample.py  F	[100%]

=======================  FAILURES  =======================
_____________________  test_answer  ______________________

def  test_answer():
>	assert  inc(3)  ==  5
E	assert  4  ==  5
E	+	where  4  =  inc(3)

test_sample.py:9:  AssertionError
===============  1  failed  in  0.15  seconds  ===============
“pytest”命令在安装 pytest 测试框架时默认生成于...\Python37\Scripts\目录。
通过上面的例子,相信你已经感受到了 pytest 的优点,它更加简单。首先,不必像 unittest
一样必须创建测试类。其次,使用 assert  断言也比使用 unittest 提供的断言方法更加简单。
不过,它也有自己的规则,即测试文件和测试函数必须以“test”开头。这也是在执行
“pytest”命令时并没有指定测试文件也可以执行 test_sample.py 文件的原因,因为该文件名
以“test”开头。
能否像 unittest 一样,通过 main()方法执行测试用例呢?当然可以,pytest 同样提供了
main()方法。
import  pytest

def  inc(x):
    return  x  +  1

def  test_answer():
    assert  inc(3)  ==  5

if  __name__  ==  '__main__':
    pytest.main()
main()方法默认执行当前文件中所有以“test”开头的函数。现在可以直接在 IDE 中运
行测试了。
9.2 pytest 的基本使用方法

因为我们已经具备了 unittest 的基础,对于单元测试框架中的概念也已经理解,所以在
学习 pytest 时要轻松许多,只需对比它与 unittest 之间的不同即可。

9.2.1  断言

在   unittest   单 元 测 试 框 架 中 提 供 了 丰 富 的 断 言 方 法 , 如   assertEqual()、assertIn()、assertTrue()、assertIs()等。pytest 单元测试框架并没有提供专门的断言方法,而是直接使用Python 的 assert 进行断言。
创建 test_assert.py 文件。
# 功能:用于计算a,b 相加的和
def add(a, b):
    return a + b


# 功能:用于判断素数
def is_prime(n):
    if n <= 1:
        return False
    for i in range(2, n):
        if n % i == 0:
            return False
        return True


# 测试相等
def test_add_1():
    assert add(3, 4) == 7


# 测试不相等
def test_add_2():
    assert add(17, 22) != 50


# 测试大于等于
def test_add_3():
    assert add(17, 22) <= 50


# 测试小于等于
def test_add_4():
    assert add(17, 22) >= 38


# 测试包含
def test_in():
    a = "hello"
    b = "he"
    assert b in a


# 测试不包含
def test_not_in():
    a = "hello"
    b = "hi"
    assert b not in a


# 判断是否为True
def test_true_1():
    assert is_prime(13)


# 判断是否为True
def test_true_2():
    assert is_prime(7) is True


# 判断是否不为True
def test_true_3():
    assert not is_prime(4)


# 判断是否不为True
def test_true_4():
    assert is_prime(6) is not True


# 判断是否为False
def test_false_1():
    assert is_prime(8) is False
上面的例子展示了 pytest 断言的用法,借助 Python 的运算符号和关键字即可轻松实现
不同数据类型的断言。

9.2.2  Fixture

Fixture 通常用来对测试方法、测试函数、测试类和整个测试文件进行初始化或还原测
试环境。创建 test_fixtures_01.py 文件。
#  功能函数
# 功能函数
def multiply(a, b):
    return a * b


# =====fixtures========
def setup_module(module):
    print("setup_module================>")


def teardown_module(module):
    print("teardown_module=============>")


def setup_function(function):
    print("setup_function------>")


def teardown_function(function):
    print("teardown_function--->")


def setup():
    print("setup----->")


def teardown():
    print("teardown-->")


# =====测试用例========
def test_multiply_3_4():
    print('test_numbers_3_4')
    assert multiply(3, 4) == 12


def test_multiply_a_3():
    print('test_strings_a_3')
    assert multiply('a', 3) == 'aaa'

这里主要用到模块级别和函数级别的 Fixture。
  setup_module/teardown_module:在当前文件中,在所有测试用例执行之前与之后
执行。
  setup_function/teardown_function:在每个测试函数之前与之后执行。
  setup/teardown:在每个测试函数之前与之后执行。这两个方法同样可以作用于
类方法。
pytest 是支持使用测试类的,同样必须以“Test”开头,注意首字母大写。在引入测试
类的情况下,Fixture 的用法如下。创建 test_fixtures_02.py 文件。
# 功能函数
def multiply(a, b):
    return a * b


class TestMultiply:
    # =====fixtures========
    @classmethod
    def setup_class(cls):
        print("setup_class=========>")

    @classmethod
    def teardown_class(cls):
        print("teardown_class=========>")

    def setup_method(self, method):
        print("setup_method----->>")

    def teardown_method(self, method):
        print("teardown_method-->>")

    def setup(self):
        print("setup----->")

    def teardown(self):
        print("teardown-->")

    # =====测试用例========
    def test_numbers_5_6(self):
        print('test_numbers_5_6')
        assert multiply(5, 6) == 30

    def test_strings_b_2(self):
        print('test_strings_b_2')
        assert multiply('b', 2) == 'bb'
这里主要用到类级别和方法级别的 Fixture。
  setup_class/teardown_class  :在当前测试类的开始与结束时执行。
  setup_method/teardown_method  :在每个测试方法开始与结束时执行。
  setup/teardown  :在每个测试方法开始与结束时执行,同样可以作用于测试函数。
运行结果。
>  pytest  -s  test_fixtures_02.py
=========================  test  session  starts  ==========================
platform  win32  --  Python  3.7.1,  pytest-4.3.0,  py-1.8.0,  pluggy-0.9.0
rootdir:  D:\git\book-code\pytest_sample\base_used,  inifile:
collected  2  items

test_fixtures_02.py
setup_class=========>
setup_method----->>
setup----->
test_numbers_5_6
.teardown-->
teardown_method-->>
setup_method----->>
setup----->
test_strings_b_2
.teardown-->
teardown_method-->>
teardown_class=========>

=======================  2  passed  in  0.03  seconds  =======================


9.2.3   参数化

当一组测试用例有固定的测试数据时,就可以通过参数化的方式简化测试用例的编写。
pytest 本身是支持参数化的,不需要额外安装插件。创建 test_parameterize.py 文件。
import  pytest
import  math

#  pytest 参数化
@pytest.mark.parametrize(
"base,  exponent,  expected",
[(2,  2,  4),
(2,  3,  8),
(1,  9,  1),
(0,  9,  0)],
ids=["case1",  "case2",  "case3",  "case4"]
)
def  test_pow(base,  exponent,  expected):
assert  math.pow(base,  exponent)  ==  expected
用法与 unittest 的参数化插件类似,通过 pytest.mark.parametrize()方法设置参数。
“base,exponent,expected”用来定义参数的名称。通过数组定义参数时,每一个元组都
是一条测试用例使用的测试数据。ids 参数默认为 None,用于定义测试用例的名称。

math 模块的 pow()方法用于计算 xy(x 的 y 次方)的值。
运行结果如下。
>  pytest  -v  test_parameterized.py
========================  test  session  starts  =========================
platform  win32  --  Python  3.7.1,  pytest-4.3.0,  py-1.8.0,  pluggy-0.9.0  --
c:\python37\python37.exe
cachedir:  .pytest_cache
rootdir:  D:\git\book-code\pytest_sample\base_used,  inifile:
collected  4  items

test_parameterize.py::test_pow[case1] PASSED	[  25%]
test_parameterize.py::test_pow[case2] PASSED	[  50%]
test_parameterize.py::test_pow[case3] PASSED	[  75%]
test_parameterize.py::test_pow[case4] PASSED	[100%]

======================  4  passed  in  0.14  seconds  ======================

“-v”参数增加测试用例冗长。不设置 ids 参数的结果如下。
test_parameterize.py::test_pow[2-2-4] PASSED	[  25%]
test_parameterize.py::test_pow[2-3-8] PASSED	[  50%]
test_parameterize.py::test_pow[1-9-1] PASSED	[  75%]
test_parameterize.py::test_pow[0-9-0] PASSED	[100%]
9.2.4   运行测试

pytest 提供了丰富的参数运行测试用例,在前面的例子中已经使用到一些参数,例如,
“-s”参数用于关闭捕捉,从而输出打印信息;“-v”参数用于增加测试用例冗长。
通过“pytest --help”可以查看帮助。
>  pytest––help

pytest 提供的参数比较多,下面只介绍常用的参数。

1.运行名称中包含某字符串的测试用例

>  pytest  -k  add  test_assert.py
========================  test  session  starts  =========================
platform  win32  --  Python  3.7.1,  pytest-4.3.0,  py-1.8.0,  pluggy-0.9.0
rootdir:  D:\git\book-code\pytest_sample\base_used,  inifile:
collected  11  items  /  7  deselected  /  4  selected

test_assert.py  ...[100%]

===============  4  passed,  7  deselected  in  0.12  seconds  ===============

在 9.2.1 节的 test_assert.py 文件中,我们写了很多测试用例,其中有 4 条是关于 add()
功能的,并且在测试用例的名称上包含了“add”字符串,因此这里可以通过“-k”来指定
在名称中包含“add”的测试用例。

2.减少测试的运行冗长

>  pytest  -q  test_assert.py
....[100%]
11  passed  in  0.03  seconds

这一次运行日志少了很多信息,“-q”用来减少测试运行的冗长;也可以使用“--quiet”
代替。





3.如果出现一条测试用例失败,则退出测试

>  pytest  -x  test_fail.py
========================  test  session  starts  =========================
platform  win32  --  Python  3.7.1,  pytest-4.3.0,  py-1.8.0,  pluggy-0.9.0
rootdir:  D:\git\book-code\pytest_sample\base_used,  inifile:
collected  2  items

test_fail.py  F

==============================  FAILURES  ==============================
_____________________________  test_fail  ______________________________

def  test_fail():
>  assert  (2  +  1)  ==  4
E  assert  (2  +  1)  ==  4

test_fail.py:5:  AssertionError
======================  1  failed  in  0.16  seconds  ======================

这在测试用例的调试阶段是有用的,当出现一条失败的测试用例时,应该先通过调试
让这条测试用例运行通过,而不是继续执行后面的测试用例。

4.运行测试目录

>  pytest  ./test_dir
========================  test  session  starts  =========================
platform  win32  --  Python  3.7.1,  pytest-4.3.0,  py-1.8.0,  pluggy-0.9.0
rootdir:  D:\git\book-code\pytest_sample,  inifile:
collected  2  items

test_dir\test_file_01.py  .    [  50%]
test_dir\test_file_02.py  .    [100%]

======================  2  passed  in  0.15  seconds  ======================

测 试 目 录 既 可 以 指 定 相 对 路 径 ( 如 ./test_dir ) , 也 可 以 指 定 绝 对 路 径 ( 如
D:\pytest_sample\test_dir)。



5.指定特定类或方法执行

>  pytest  test_fixtures_02.py::TestMultiply::test_numbers_5_6
========================  test  session  starts  =========================
platform  win32  --  Python  3.7.1,  pytest-4.3.0,  py-1.8.0,  pluggy-0.9.0
rootdir:  D:\git\book-code\pytest_sample\base_used,  inifile:
collected  1  item

test_fixtures_02.py  . [100%]

======================  1  passed  in  0.05  seconds  ======================

这里指定运行 test_fixtures_02.py 文件中 TestMultiply 类下的 test_numbers_5_6()方法,
文件名、类名和方法名之间用“::”符号分隔。

6.通过 main()方法运行测试

import  pytest

if  __name__  ==  '__main__':
pytest.main(['-s',  './test_dir'])

创建 run_tests.py 文件,在文件中通过数组指定参数,每个参数为数组中的一个元素。
9.2.5   生成测试报告

pytest 支持生成多种格式的测试报告。

1.生成 JUnit XML 文件

>  pytest  ./test_dir   --junit-xml=./report/log.xml

XML 类型的日志主要用于存放测试结果,方便我们利用里面的数据定制自己的测试报
告。XML 格式的测试报告如图 9-1 所示。























图 9-1  XML 格式的测试报告

2.生成在线测试报告

>  pytest  ./test_dir  --pastebin=all
========================  test  session  starts  =========================
platform  win32  --  Python  3.7.1,  pytest-4.3.0,  py-1.8.0,  pluggy-0.9.0  --
c:\python37\python37.exe
rootdir:  D:\git\book-code\pytest_sample,  inifile:
collected  2  items

test_dir\test_file_01.py  .    [  50%]
test_dir\test_file_02.py  .    [100%]

======================  2  passed  in  0.03  seconds  ======================
================  Sending  information  to  Paste  Service  ================
pastebin  session-log:  https://bpaste.net/show/61aabfff6cfd

上述代码可生成一个 session-log 链接,复制链接,通过浏览器打开,会得到一张 HTML
格式的测试报告,如图 9-2 所示。



1

















图 9-2  HTML 格式的测试报告

9.2.6  conftest.py

conftest.py  是  pytest  特有的本地测试配置文件,既可以用来设置项目级别的  Fixture,
也可以用来导入外部插件,还可以用来指定钩子函数。

创建 test_project/conftest.py 测试配置文件。

import  pytest

#  设置测试钩子
@pytest.fixture()
def  test_url():
return  "http://www.baidu.com"

创建 test_project/test_sub.py 测试用例文件。

def  test_baidu(test_url):
print(test_url)

这里创建的函数可以直接调用  conftest.py 文件中的 test_url()钩子函数,测试结果如下。

>  pytest  -s  -v  test_project\
===========================  test  session  starts  ============================
platform  win32  --  Python  3.7.1,  pytest-4.3.0,  py-1.8.0,  pluggy-0.9.0  --
c:\python37\python37.exe
cachedir:  .pytest_cache
rootdir:  D:\git\book-code\pytest_sample,  inifile:



第 


collected  1  item

test_project/test_web.py::test_baidu  http://www.baidu.com
PASSED

=========================  1  passed  in  0.02  seconds  =========================

需要说明的是,conftest.py 只作用于它所在的目录及子目录。

9.3    pytest 扩展

Pytest 可以扩展非常多的插件来实现各种功能,这里介绍几个对做 Web 自动化测试非
常有用的插件。

9.3.1  pytest-html

pytest-html 可以生成 HTML 格式的测试报告。首先,通过 pip 命令安装 pytest-html 扩
展。

>  pip  install  pytest-html

其次,运行测试用例,并生成测试报告。

>  pytest  ./  --html=./report/result.html

最后,在 report 目录下打开 result.html,pytest-html 测试报告如图 9-3 所示。















图 9-3  pytest-html 测试报告





pyest-html 还支持测试用例失败的截图,这对于 Web 自动化测试来说非常有用,在 9.4
节详细介绍。

9.3.2  pytest-rerunfailures

pytest-rerunfailures 可以在测试用例失败时进行重试。

>  pip  install  pytest-rerunfailures

创建 test_ rerunfailures.py。

def  test_fail_rerun():
assert  2  +  2  ==  5

通过“--reruns”参数设置测试用例运行失败后的重试次数。

>  pytest  -v  test_rerunfailures.py  --reruns  3
===========================  test  session  starts  ============================
platform  win32  --  Python  3.7.1,  pytest-4.3.0,  py-1.8.0,  pluggy-0.9.0  --
c:\python37\python37.exe
cachedir:  .pytest_cache
metadata:  {'Python':  '3.7.1',  'Platform':  'Windows-10-10.0.17763-SP0',
'Packages':  {'pytest':  '4.3.0',  'py':  '1.8.0',  'pluggy':  '0.9.0'},  'Plugins':
{'rerunfailures':  '6.0',  'metadata':  '1.8.0',  'html':  '1.20.0'},  'JAVA_HOME':
'D:\\Java\\jdk1.8.0_144'}
rootdir:  D:\git\book-code\pytest_sample,  inifile:
plugins:  rerunfailures-6.0,  metadata-1.8.0,  html-1.20.0
collected  1  item

test_rerunfailures.py::test_fail_rerun RERUN   [100%]
test_rerunfailures.py::test_fail_rerun RERUN   [100%]
test_rerunfailures.py::test_fail_rerun RERUN   [100%]
test_rerunfailures.py::test_fail_rerun FAILED  [100%]

=================================  FAILURES  =================================
_____________________________  test_fail_rerun  ______________________________

def  test_fail_rerun():
>  assert  2  +  2  ==  5
E  assert  4  ==  5
E  -4
E  +5

test_rerunfailures.py:3:  AssertionError




====================  1  failed,  3  rerun  in  0.10  seconds  =====================

从运行结果可以看到,在测试用例运行失败后进行了 3 次重试。因为 Web 自动化测试
会因为网络等因素导致测试用例运行失败,而重试机制可以增加测试用例的稳定性。

9.3.3  pytest-parallel 扩展

pytest-parallel 扩展可以实现测试用例的并行运行。

>  pip  install  pytest-parallel

创建 test_ parallel.py,在每条测试用例中分别设置 sleep()来模拟运行时间较长的测试用
例。

from  time  import  sleep

def  test_01():
sleep(3)

def  test_02():
sleep(5)

def  test_03():
sleep(6)

不使用线程运行测试用例。

>  pytest  -q  test_parallel.py
...[100%]
3  passed  in  14.05  seconds

参数“--tests-per-worker”用来指定线程数,“auto”表示自动分配。

pytest  -q  test_parallel.py  --tests-per-worker  auto
pytest-parallel:  1  worker  (process),  3  tests  per  worker  (threads)
...    [100%]
3  passed  in  6.02  seconds

运行时间由 14.05s 被缩短到 6.02s,因为运行时间最长的测试用例为 6s。
pytest-parallel 的更多用法如下所示。

#  runs  2  workers  with  1  test  per  worker  at  a  time
>  pytest  --workers  2





#  runs  4  workers  (assuming  a  quad-core  machine)  with  1  test  per  worker
>  pytest  --workers  auto

#  runs  1  worker  with  4  tests  at  a  time
>  pytest  --tests-per-worker  4

#  runs  1  worker  with  up  to  50  tests  at  a  time
>  pytest  --tests-per-worker  auto

#  runs  2  workers  with  up  to  50  tests  per  worker
>  pytest  --workers  2  --tests-per-worker  auto

虽然并行运行测试可以非常有效地缩短测试的运行时间,但是 Web 自动化测试本身非
常脆弱,在并行运行测试时很可能会产生相互干扰,从而导致测试用例失败,因此建议谨
慎使用。

9.4    构建 Web 自动化测试项目

相比 unittest 单元测试框架,pytest 更适合用来做 UI 自动化测试,它提供了以下功能。
(1)在 unittest 中,浏览器的启动或关闭只能基于测试方法或测试类;pytest 可以通过
conftest.py  文件配置全局浏览器的启动或关闭,整个自动化测试项目的运行只需启动或关
闭一次浏览器即可,这将大大节省测试用例执行时间。

(2)测试用例运行失败截图。unittest 本身是不支持该功能的,pytest-html 可以实现测
试用例运行失败自动截图,只需在 conftest.py 中做相应的配置即可。
(3)测试用例运行失败重跑。UI 自动化测试的稳定性一直是难题,虽然可以通过元素
等待来增加稳定性,但有很多不可控的因素(如网络不稳定)会导致测试用例运行失败,
pytest-rerunfailures 可以轻松实现测试用例运行失败重跑。

9.4.1  项目结构介绍

pyautoTest 项目是对基于 pytest 进行 UI 自动化测试实践的总结,在该项目的基础上,
可以快速编写自己的自动化测试用例。

GitHub 地址:https://github.com/defnngj/pyautoTest。



第 9 章   pytest 单元测试框架  ∣  167


1.pyautoTest 项目结构如图 9-4 所示。















图 9-4  pyautoTest 项目结构
  page/:用于存放 page 层的封装。
  test_dir/:测试用例目录。
  test_report/:测试报告目录。
  conftest.py:pytest 配置文件。
  run_tests.py:测试运行文件。

2.命名与设计规范

(1)对于 page 层的封装存放于 page/目录,命名规范为“xxx_page.py”。
(2)对于测试用例的编写存放于 test_dir/目录,命名规范为“test_xxx.py”。
(3)每一个功能点对应一个测试类,并且以“Test”开头,如“TestLogin”“TestSearch”等。
(4)在一个测试类下编写功能点的所有的测试用例,如“test_login_user_null”、
“test_login_pawd_null”及“test_login_success”等。

3.克隆与安装依赖

(1)安装 Git 版本控制工具,将 pyautoTest 项目克隆到本地。

>  git  clone  https://github.com/defnngj/pyautoTest

(2)通过 pip 命令安装依赖。





>  pip  install  -r  requirements.txt

4.依赖库说明

  selenium:Web UI 自动化测试。
  pytest:Python 第三方单元测试框架。
  pytest-html:pytest 扩展,生成 HTML 格式的测试报告。
  pytest-rerunfailures:pytest 扩展,实现测试用例运行失败重跑。
  click:命令行工具开发库。
  poium:基于 Selenium/appium 的 Page Object 测试库。

9.4.2  主要代码实现

首先,封装页面 Page 层,创建 page/baidu_page.py 文件。

from  page_objects  import  Page,  PageElement,  PageElements

class  BaiduPage(Page):
search_input  =  PageElement(id_="kw",  describe="搜索框")
search_button  =  PageElement(id_="su",  describe="搜索按钮")
settings  =  PageElement(link_text="设置",  describe="设置下拉框")
search_setting  =  PageElement(css=".setpref",  describe="搜索设置选项")
save_setting  =  PageElement(css=".prefpanelgo",  describe="保存设置")

#  定位一组元素
search_result  =  PageElements(xpath="//div/h3/a",  describe="搜索结果")

在第 8 章详细介绍了 poium 测试库的使用方法,基于该测试库可以非常简单地封装页
面 Page 层的元素。
其次,编写测试用例,创建 test_dir/test_baidu.py 文件。

import  sys
from  time  import  sleep
from  os.path  import  dirname,  abspath
sys.path.insert(0,  dirname(dirname(abspath(__file__))))
from  page.baidu_page  import  BaiduPage

class  TestSearch:
"""百度搜索"""




def  test_baidu_search_case(self,  browser,  base_url):
"""  百度搜索:pytest  """
page  =  BaiduPage(browser)
page.get(base_url)
page.search_input  =  "pytest"
page.search_button.click()
sleep(2)
assert  browser.title  ==  "pytest_百度搜索"

创建测试 test_baidu_search()函数,接收 browser  和  base_url 钩子函数。这两个函数需
要在 conftest.py 文件中定义。接下来,创建测试方法调用 BaiduPage 类,传入 browser 驱动,
调用 BaiduPage 类和父类所实现的方法及定义来完成相应的操作。
在测试用例中,可以将注意力集中在测试用例设计本身的操作上,而不需要关心浏览
器驱动、访问的 URL 以及测试用例运行失败截图,因为这些都已经在 conftest.py 文件中配
置好了。

1.conftest.py 文件之自动化配置

…
############################

#  配置浏览器驱动类型(Chrome/Firefox)
driver  =  "chrome"

#  配置运行的  URL
url  =  "https://www.baidu.com"

#  设置失败重跑次数
rerun  =  "3"

#  运行测试用例的目录或文件
cases_path  =  "./test_dir/"

############################

…

在不熟悉整个项目配置之前,需要关心以上几个配置,在代码中已经分别加了注释,
这里不再说明。





2.conftest.py 文件之浏览器配置

#  配置浏览器驱动类型
driver_type  =  "chrome"
…

#  启动浏览器
@pytest.fixture(scope='session',  autouse=True)
def  browser():
"""
定义全局浏览器驱动
:return:
"""
global  driver
global  driver_type

if  driver_type  ==  "chrome":
#  本地 Chrome 浏览器
driver  =  webdriver.Chrome()
driver.set_window_size(1920,  1080)

elif  driver_type  ==  "firefox":
#  本地 Firefox 浏览器
driver  =  webdriver.Firefox()
driver.set_window_size(1920,  1080)

elif  driver_type  ==  "chrome-headless":
#  chrome  headless 模式
chrome_options  =  CH_Options()
chrome_options.add_argument("--headless")
chrome_options.add_argument('--disable-gpu')
chrome_options.add_argument("--window-size=1920x1080")
driver  =  webdriver.Chrome(chrome_options=chrome_options)

elif  driver_type  ==  "firefox-headless":
#  firefox  headless 模式
firefox_options  =  FF_Options()
firefox_options.headless  =  True
driver  =  webdriver.Firefox(firefox_options=firefox_options)

elif  driver_type  ==  "grid":
#  通过远程节点运行
driver  =  Remote(command_executor='http://10.2.16.182:4444/wd/hub',





desired_capabilities={
"browserName":  "chrome",
})
driver.set_window_size(1920,  1080)

else:
raise  NameError("driver 驱动类型定义错误!")

return  driver


#  关闭浏览器
@pytest.fixture(scope="session",  autouse=True)
def  browser_close():
yield  driver
driver.quit()
print("test  end!")

Selenium 在启动浏览器时会创建一个 session,当通过@pytest.fixture()装饰浏览器开启
和关闭函数时,scope 参数需要设置为“session”。browser()函数用于定义浏览器,根据全
局变量 driver_type 的定义创建不同的浏览器驱动。browser_close()函数用于实现浏览器的关
闭。

3.conftest.py 文件之失败截图配置

…

@pytest.mark.hookwrapper
def  pytest_runtest_makereport(item):
"""
用于向测试用例中添加开始时间、内部注释和失败截图等
:param  item:
"""
pytest_html  =  item.config.pluginmanager.getplugin('html')
outcome  =  yield
report  =  outcome.get_result()
report.description  =  str(item.function.__doc__)
extra  =  getattr(report,  'extra',  [])
if  report.when  ==  'call'  or  report.when  ==  "setup":
xfail  =  hasattr(report,  'wasxfail')
if  (report.skipped  and  xfail)  or  (report.failed  and  not  xfail):
case_path  =  report.nodeid.replace("::",  "_")  +  ".png"





if  "["  in  case_path:
case_name  =  case_path.split("-")[0]  +  "].png"
else:
case_name  =  case_path
capture_screenshot(case_name)
img_path  =  "image/"  +  case_name.split("/")[-1]
if  img_path:
html  =  '<div><img  src="%s"  alt="screenshot"
style="width:304px;height:228px;"  '  \
'οnclick="window.open(this.src)"  align="right"/></div>'  %
img_path
extra.append(pytest_html.extras.html(html))
report.extra  =  extra


def  capture_screenshot(case_name):
"""
配置测试用例失败截图路径
:param  case_name:  用例名
:return:
"""
global  driver
file_name  =  case_name.split("/")[-1]
new_report_dir  =  new_report_time()
if  new_report_dir  is  None:
raise  RuntimeError('没有初始化测试目录')
image_dir  =  os.path.join(REPORT_DIR,  new_report_dir,  "image",  file_name)
driver.save_screenshot(image_dir)

…

这里的配置会复杂一些,核心参考 pytest-html 文档。pytest_runtest_makereport()钩子函
数的主要功能是判断每条测试用例的运行情况,当测试用例错误或失败后会调用
capture_screenshot()函数进行截图,并将测试用例的“文件名+类名+方法名”作为截图的名
称,保存于 image/目录中。
pytest-html 会生成一张 HTML 格式的测试报告,那么如何将截图插入 HTML 格式的测
试报告中呢?核心就是添加<img>标签,并通过 src 属性指定图片的路径。明白了这一点后,
测试用例失败自动截图就很好理解了。





9.4.3  测试用例的运行与测试报告

在整个项目中还有一个关键文件 run_tests.py,它是用来执行整个项目的测试用例的。
…

@click.command()
@click.option('-m',  default=None,  help='输入运行模式:run  或  debug.')
def  run(m):
if  m  is  None  or  m  ==  "run":
print("回归模式,执行完成,生成测试结果")
now_time  =  time.strftime("%Y_%m_%d_%H_%M_%S")
init_env(now_time)
html_report  =  os.path.join(REPORT_DIR,  now_time,  "report.html")
xml_report  =  os.path.join(REPORT_DIR,  now_time,  "junit-xml.xml")
pytest.main(["-s",  "-v",  cases_path,
"--html="  +  html_report,
"--junit-xml="  +  xml_report,
"--self-contained-html",
"--reruns",  rerun])
elif  m  ==  "debug":
print("debug 模式运行测试用例:")
pytest.main(["-v",  "-s",  cases_path])
print("运行结束!!")


if  __name__  ==  '__main__':
run()

这里用到了命令行工具开发库 click。click 提供了两种运行模式:run 模式和 debug 模
式。不同模式下的 pytest 的执行参数不同。查看帮助如下。

>  python  run_tests.py  --help
Usage:  run_tests.py  [OPTIONS]

Options:
-m  TEXT   输入运行模式:run  或  debug.
--help Show  this  message  and  exit.

注意:不要试图在 IDE 中直接运行 run_tests.py 文件,因为 click 并不允许这么
做。请在 Windows 命令符/Linux 终端下运行。

运行方式如下。



174  ∣  Selenium 3 自动化测试实战——基于 Python 语言


>  python  run_tests.py  -m  run
回归模式,执行完成,生成测试结果
=============================  test  session  starts
=============================
platform  win32  --  Python  3.7.1,  pytest-4.3.0,  py-1.8.0,  pluggy-0.9.0  --
C:\Python37\python.exe
cachedir:  .pytest_cache
metadata:  {'Python':  '3.7.1',  'Platform':  'Windows-10-10.0.17134-SP0',
'Packages':  {'pytest':  '4.3.0',  'py':  '1.8.0',  'pluggy':  '0.9.0'},  'Plugins':
{'rerunfailures':  '6.0',  'metadata':  '1.8.0',  'html':  '1.20.0'}}
rootdir:  D:/git/pyautoTest,  inifile:
plugins:  rerunfailures-6.0,  metadata-1.8.0,  html-1.20.0
collected  5  items

test_dir/test_baidu.py::TestSearch::test_baidu_search_case  PASSED
test_dir/test_baidu.py::TestSearch::test_baidu_search[case1]  PASSED
test_dir/test_baidu.py::TestSearch::test_baidu_search[case2]  PASSED
test_dir/test_baidu.py::TestSearch::test_baidu_search[case3]  PASSED
test_dir/test_baidu.py::TestSearchSettings::test_baidu_search_setting  test
end!
PASSED

generated  xml  file:
D:\git\pyautoTest\test_report\2018_12_27_23_44_56\junit-xml.xml
-------  generated  html  file:  D:\git\pyautoTest\test_report\report.html
--------
========================== 5 passed in 22.23 seconds ==========================

生成的 HTML 测试报告在 test_report 目录下,test_report 目录如图 9-5 所示。当测试用
例运行失败时自动截图,并显示在 HTML 测试报告中,HTML 测试报告如图 9-6 所示。










图 9-5  test_report 目录



第 9 章   pytest 单元测试框架  ∣  175























图 9-6  HTML 测试报告

Python+Selenium+pytest+PO模式是一种常用的Web UI自动化测试框架,下面是一些基本的步骤: 1. 安装pytest和Selenium:使用pip install pytest selenium命令进行安装。 2. 创建pytest测试用例:创建一个py文件,使用pytest框架编写测试用例,可以使用Selenium进行web页面的操作和数据的验证。 3. 创建PO模式的页面对象:使用Page Object模式构建页面对象,将页面的元素和操作封装在一个类中,方便管理和维护。 4. 运行pytest测试:使用pytest运行测试用例,并查看测试报告。 5. 使用pytest插件:pytest提供了许多插件,例如pytest-html、pytest-xdist等,可以用来生成HTML测试报告、运行分布式测试等。 下面是一个简单的示例代码: ```python # conftest.py文件 from selenium import webdriver import pytest @pytest.fixture(scope='module') def driver(): driver = webdriver.Chrome() yield driver driver.quit() # test_login.py文件 from pages.login_page import LoginPage def test_login(driver): login_page = LoginPage(driver) login_page.open() login_page.input_username('test') login_page.input_password('test123') login_page.click_submit() assert login_page.get_login_result() == '登录成功' # login_page.py文件 from selenium.webdriver.common.by import By class LoginPage: url = 'http://localhost/login' def __init__(self, driver): self.driver = driver def open(self): self.driver.get(self.url) def input_username(self, username): self.driver.find_element(By.ID, 'username').send_keys(username) def input_password(self, password): self.driver.find_element(By.ID, 'password').send_keys(password) def click_submit(self): self.driver.find_element(By.ID, 'submit').click() def get_login_result(self): return self.driver.find_element(By.ID, 'result').text ``` 在上面的示例中,我们使用了pytest的fixture机制来管理WebDriver对象的生命周期,使用PO模式的页面对象来封装登录页面的元素和操作。我们可以通过执行pytest test_login.py命令来运行测试,并生成测试报告。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

老板来片烤面包

君子博学于文,赠之以礼,谢君~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值