pytest 是一个第三方单元测试框架,更加简单、灵活,而且提供了更加丰富的扩展,弥补了 unittest 在做 Web 自动化测试时的一些不足。
1. pytest 简单例子
pytest 支持 pip 安装,pip install pytest
创建 test_sample.py 文件,代码如下:
def inc(x):
return x + 1
def test_answer():
assert inc(3) == 5
命令行窗口,进入到 test_sample.py 文件所在目录,直接输入 “pytest” 即可。运行结果如下: 从上看出,pytest 的优点是它更加简单,不必创建测试类,使用 assert 断言也更加简单。
不过,pytest 也有自己的规则,即测试文件和测试函数必须以“test”开头。所以运行时才不用指定文件。
如果要像 unittest 通过 main() 方法执行测试用例,需要修改下 test_sample.py 文件:
import pytest
def inc(x):
return x + 1
def test_answer():
assert inc(3) == 5
if __name__ == '__main__':
pytest.main()
main()方法默认执行当前文件中所有以“test”开头的函数。这样就可以直接在 IDE 中运行测试了。
2. pytest 的基本使用方法
前面我们学习了 unittest 的基础,现在只需对比 pytest 与 unittest 之间的不同即可。
(1)断言
unittest 单元测试框架中提供了丰富的断言方法,而pytest 单元测试框架并没有提供专门的断言方法,而是直接使用 Python 的 assert 进行断言。
下面创建 test_assert.py 文件,介绍 pytest 断言的用法:(借助 Python 的运算符号和关键字即可轻松实现不同数据类型的断言。)
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_add1():
assert add(3, 4) == 7
def test_add2():
assert add(17, 22) != 50
def test_add3():
assert add(17, 22) <= 50
def test_add4():
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
def test_true_1():
assert is_prime(13)
def test_true_2():
assert is_prime(7) is True
def test_true_3():
assert not is_prime(4)
def test_true_4():
assert is_prime(6) is not True
def test_false_1():
assert is_prime(8) is False
(2)Fixture
Fixture 通常用来对测试方法、测试函数、测试类和整个测试文件进行初始化或还原测试环境。
创建 test_fixtures_01.py 文件:
def multiply(a, b):
return a * b
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_4')
assert multiply('a', 3) == 'aaa'
然后命令行进入到该目录,执行命令 “pytest -s test_fixture_01.py”,运行结果如下: “-s”参数用于关闭捕捉,从而输出打印信息。 这里主要用到模块级别和函数级别的 Fixture。
- setup_module/teardown_module:在当前文件中,在所有测试用例执行之前与之后执行。
- setup_function/teardown_function:在每个测试函数之前与之后执行。
- setup/teardown:在每个测试函数之前与之后执行。(同样可以作用于类方法。)
pytest 是支持使用测试类的,同样必须以“Test”开头,注意首字母大写。 下面引入测试类,创建 test_fixtures_02.py 文件:
def multiply(a, b):
return a * b
class TestMultiply:
@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 :在每个测试方法开始与结束时执行。(同样可以作用于测试函数)
(3)参数化
当一组测试用例有固定的测试数据时,就可以通过参数化的方式简化测试用例的编写。 pytest 本身是支持参数化的,不需要额外安装插件。
创建 test_parameterize.py 文件:
import pytest
import math
@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
“base,exponent,expected”用来定义参数的名称。 通过数组定义参数时,每一个元组都是一条测试用例使用的测试数据。 ids 参数默认为 None,用于定义测试用例的名称。不设置dis值时运行结果如下: math 模块的 pow()方法用于计算 xy(x 的 y 次方)的值。 命令行执行命令中,“-v”参数用于增加测试用例冗长。
(4)运行测试
pytest 提供了丰富的参数运行测试用例,前面我们知道,“-s”参数用于关闭捕捉,从而输出打印信息;“-v”参数用于增加测试用例冗长。 通过“pytest --help”可以查看帮助。
下面介绍常用的参数 a. 运行名称中包含某字符串的测试用例 test_assert.py 文件中有四个测试用例带字符串“add”,这里通过“-k”来指定在名称中包含“add”的4个测试用例。
**b.**减少测试的运行冗长 日志少了很多信息,“-q”用来减少测试运行的冗长;也可以使用“–quiet”代替。
**c.**如果出现一条测试用例失败,则退出测试 这在测试用例的调试阶段是有用的,有用例失败时应该先通过调试成功,再来执行后面测试用例。 创建 test_fail.py 文件:
def test_fail():
assert (2 + 1) == 4
def test_success():
assert (2 + 3) == 5
**d.**运行测试目录 测试目录既可以指定相对路径(如 ./test_dir ),也可以指定绝对路径(如D:\xxx\xxx…\test_dir)。
**e.**指定特定类或方法执行 这里指定运行 test_fixtures_02.py 文件中 TestMultiply 类下的 test_numbers_5_6()方法,文件名、类名和方法名之间用“::”符号分隔。
**f.**通过main()方法运行测试 创建 run_tests.py 文件,在文件中通过数组指定参数,每个参数为数组中的一个元素。(直接运行即可)
import pytest
if __name__ == '__main__':
pytest.main(['-s', './test_dir'])
(5)生成测试报告
pytest 支持生成多种格式的测试报告。
a. 生成 JUnit XML 文件
pytest ./test_dir --junit-xml=./report/log.xml XML 类型的日志主要用于存放测试结果,方便我们利用里面的数据定制自己的测试报告。
b. 生成在线测试报告
pytest ./test_dir --pastebin=all 上述代码可生成一个 session-log 链接,复制链接,通过浏览器打开,会得到一张 HTML格式的测试报告(此处产生 HTTP 405 错误,暂未解决)
(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_web.py 测试用例文件:(这里的函数直接调用congtest.py 文件的 test_url()钩子函数)
def test_baidu(test_url):
print(test_url)
需要说明的是,conftest.py 只作用于它所在的目录及子目录。
3.pytest 扩展
Pytest 可以扩展非常多的插件来实现各种功能,介绍几个对 Web 自动化测试非常有用的插件。
(1)pytest-html
pytest-html 可以生成 HTML 格式的测试报告。通过 pip 安装,pip install pytest-html。 pyest-html 还支持测试用例失败的截图(第四节)
pytest ./ --html=./report/result.html
(2)pytest-rerunfailures
pytest-rerunfailures 可以在测试用例失败时进行重试。通过 pip 安装,pip install pytest-rerunfailures
创建 test_rerunfailures.py
def test_fail_rerun():
assert 2 + 2 == 5
pytest -v test_rerunfailures.py --reruns 3 通过“–reruns”参数设置测试用例运行失败后的重试次数。 因为 Web 自动化测试会因为网络等因素导致测试用例运行失败,而重试机制可以增加测试用例的稳定性。
(3)pytest-parallel 扩展
pytest-parallel 扩展可以实现测试用例的并行运行。使用 pip 安装,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)
不使用线程运行测试用例: 使用线程运行测试用例:(参数“–tests-per-worker”用来指定线程数,“auto”表示自动分配)
pytest -q test_parallel.py --tests-per-worker auto
该命令本地运行失败,报错:PermissionError: [WinError 5] 拒绝访问。 根据网上的解决方法,尝试修改 python 的权限(python.exe - 右键属性 - 安全 - 编辑 - Users(LAPTOP-ATGBMF1K\Users))- 完全控制√允许- 应用 - 确定 ) 再重新执行上述命令,报错:OSError: [WinError 87] 参数错误。 网上的解决方法是,要么降低 python 的版本,要么降低 pytest 的版本试试看。(未尝试该方法)
pytest-parallel 的更多用法:
pytest --workers 2
pytest --workers auto
pytest --tests-per-worker 4
pytest --tests-per-worker auto
pytest --workers 2 --tests-per-worker auto
并行运行测试可以非常有效地缩短测试的运行时间,但是 Web 自动化测试本身非常脆弱,在并行运行测试时很可能会产生相互干扰,从而导致测试用例失败,因此建议谨慎使用。
4.构建 Web 自动化测试项目
相比 unittest 单元测试框架,pytest 更适合用来做 UI 自动化测试,它提供以下功能:
- 在 unittest 中,浏览器的启动或关闭只能基于测试方法或测试类;pytest 可以通过 conftest.py 文件配置全局浏览器的启动或关闭,整个自动化测试项目的运行只需启动或关闭一次浏览器即可,节省测试用例执行时间。
- 测试用例运行失败截图。unittest 本身不支持该功能,pytest-html 可以实现测 试用例运行失败自动截图,只需在 conftest.py 中做相应的配置即可。
- 测试用例运行失败重跑。UI 自动化测试的稳定性一直是难题,虽然可以通过元素等待来等待来增加稳定性,但有很多不可控的因素(如网络不稳定)导致测试用例运行失败,pytest-rerunfailures 可以轻松实现测试用例运行失败重跑。
(1)项目结构介绍
书本作者虫师放在GitHub上的 pyautoTest 项目是对基于 pytest 进行 UI 自动化测试实践的总结,在该项目的基础上,可以快速编写自己的自动化测试用例。
GitHub 地址:https://github.com/defnngj/pyautoTest。
a. pyautoTest 项目结构如图:
- page/:用于存放 page 层的封装。
- test_dir/:测试用例目录。
- test_report/:测试报告目录。
- conftest.py:pytest 配置文件。
- run_tests.py:测试运行文件。
b. 命名与设计规范
- 对于 page 层的封装存放于 page/目录,命名规范为“xxx_page.py”。
- 对于测试用例的编写存放于 test_dir/目录,命名规范为“test_xxx.py”。
- 每一个功能点对应一个测试类,并且以“Test”开头,如“TestLogin”“TestSearch”等。
- 在一个测试类下编写功能点的所有的测试用例,如“test_login_user_null”、“test_login_pawd_null”及“test_login_success”等。
c. 克隆与安装依赖
- 安装 Git 版本控制工具,将 pyautoTest 项目克隆到本地。(或者直接访问上面GitHub地址,下载zip格式文件到本地)
- 通过 pip 命令安装依赖。pip install -r requirements.txt (执行命令前,命令行需要进入到该文件所在项目)
d. 依赖库说明
- selenium:Web UI 自动化测试。
- pytest:Python 第三方单元测试框架。
- pytest-html:pytest 扩展,生成 HTML 格式的测试报告。
- pytest-rerunfailures:pytest 扩展,实现测试用例运行失败重跑。
- click:命令行工具开发库。
- poium:基于 Selenium/appium 的 Page Object 测试库。
(2)主要代码实现
封装页面 Page 层,创建 page/baidu_page.py 文件。
from poium import Page, Element
class BaiduPage(Page):
search_input = Element(id_="kw", describe="搜索框")
search_button = Element(id_="su", describe="搜索按钮")
settings = Element(css="#s-usersetting-top", describe="设置")
search_setting = Element(css="#s-user-setting-menu > div > a.setpref", describe="搜索设置")
save_setting = Element(link_text="保存设置", describe="保存设置")
编写测试用例,创建 test_dir/test_baidu.py 文件。
import sys
from time import sleep
import pytest
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):
page = BaiduPage(browser)
page.get(base_url)
page.search_input = "pytest"
page.search_button.click()
sleep(2)
assert browser.title == "pytest_百度搜索"
test_baidu_search_case()函数,接收 conftest.py 文件中定义的 browser 和 base_url 钩子函数。
在测试用例中,可以将注意力集中在测试用例设计本身的操作上,而不需要关心浏览器驱动、访问的 URL 以及测试用例运行失败截图,这些都在 conftest.py 文件中配置好。
a. conftest.py 文件之自动配置(这里用 config.py 文件来放这些自动配置,然后再在conftest.py 文件中调用)
import os
PRO_PATH = os.path.dirname(os.path.abspath(__file__))
class RunConfig:
"""
运行测试配置
"""
cases_path = os.path.join(PRO_PATH, "test_dir", "test_baidu.py")
driver_type = "chrome"
url = "https://www.baidu.com"
rerun = "1"
max_fail = "5"
driver = None
NEW_REPORT = None
b. conftest.py 文件之浏览器配置
config.py 文件中配置了浏览器类型:driver_type = “chrome”
@pytest.fixture(scope='session', autouse=True)
def browser():
"""
全局定义浏览器驱动
:return:
"""
global driver
if RunConfig.driver_type == "chrome":
driver = webdriver.Chrome()
driver.maximize_window()
elif RunConfig.driver_type == "firefox":
driver = webdriver.Firefox()
driver.maximize_window()
elif RunConfig.driver_type == "chrome-headless":
chrome_options = CH_Options()
chrome_options.add_argument("--headless")
chrome_options.add_argument('--disable-gpu')
driver = webdriver.Chrome(options=chrome_options)
elif RunConfig.driver_type == "firefox-headless":
firefox_options = FF_Options()
firefox_options.headless = True
driver = webdriver.Firefox(firefox_options=firefox_options)
elif RunConfig.driver_type == "grid":
driver = Remote(command_executor='http://localhost:4444/wd/hub',
desired_capabilities={
"browserName": "chrome",
})
driver.set_window_size(1920, 1080)
else:
raise NameError("driver驱动类型定义错误!")
RunConfig.driver = 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 的定义创建不同的浏览器驱动。
c. 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 = description_html(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_screenshots(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_screenshots(case_name):
"""
配置用例失败截图路径
:param case_name: 用例名
:return:
"""
global driver
file_name = case_name.split("/")[-1]
if RunConfig.NEW_REPORT is None:
raise NameError('没有初始化测试报告目录')
else:
image_dir = os.path.join(RunConfig.NEW_REPORT, "image", file_name)
RunConfig.driver.save_screenshot(image_dir)
...
核心参考 pytest-html 文档。 pytest_runtest_makereport()钩子函数的主要功能是判断每条测试用例的运行情况,当测试用例错误或失败后会调用 capture_screenshot()函数进行截图,并将测试用例的“文件名+类名+方法名”作为截图的名称,保存于 image/目录中。
pytest-html 会生成一张 HTML 格式的测试报告,将截图插入 HTML 格式测试报告的核心就是添加标签,并通过 src 属性指定图片的路径。
(3)测试用例的运行与测试报告
项目中的关键文件 run_tests.py,它用来执行整个项目的测试用例。
...
@click.command()
@click.option('-m', default=None, help='输入运行模式:run 或 debug.')
def run(m):
if m is None or m == "run":
logger.info("回归模式,开始执行??!")
now_time = time.strftime("%Y_%m_%d_%H_%M_%S")
RunConfig.NEW_REPORT = os.path.join(REPORT_DIR, now_time)
init_env(RunConfig.NEW_REPORT)
html_report = os.path.join(RunConfig.NEW_REPORT, "report.html")
xml_report = os.path.join(RunConfig.NEW_REPORT, "junit-xml.xml")
pytest.main(["-s", "-v", RunConfig.cases_path,
"--html=" + html_report,
"--junit-xml=" + xml_report,
"--self-contained-html",
"--maxfail", RunConfig.max_fail,
"--reruns", RunConfig.rerun])
logger.info("运行结束,生成测试报告??!")
elif m == "debug":
print("debug模式,开始执行!")
pytest.main(["-v", "-s", RunConfig.cases_path])
print("运行结束!!")
if __name__ == '__main__':
run()
上述代码使用了命令行工具开发库 click。click 提供了两种运行模式:run 模式和 debug 模式。不同模式下的 pytest 的执行参数不同。 运行命令: python run_tests.py -m run 生成的 HTML 测试报告在 test_report 目录下,当测试用例运行失败时自动截图,并显示在 HTML 测试报告中。
|