Pytest测试框架基础及进阶
Pytest测试框架基础
Pytest测试框架介绍
Pytest是Python一款三方测试框架,用于编写和运行单元测试、集成测试和功能测试。Pytest测试框架具有简单、灵活、易于扩展等特点,被广泛应用于Python项目的测试工作中。
Pytest主要特点:
- 简单易用:Pytest测试框架的API简单易用,可以快速编写测试用例。
- 灵活多样:Pytest测试框架支持多种测试方式,包括函数式测试、类式测试、参数化测试、fixture测试等。
- 插件机制:Pytest测试框架支持插件机制,可以通过插件扩展测试框架的功能。
- 断言机制:Pytest测试框架支持多种断言方式,包括assert语句、assert关键字、assert表达式等。
- 报告机制:Pytest测试框架支持生成多种测试报告,包括控制台报告、HTML报告、JUnit报告等。
安装方法:
$ pip install pytest
Pytest基本用法
编写及运行测试用例
- 新建test开头的测试脚本,如test_calc.py,编写测试函数 或 测试类
def test_add(): # 测试函数需以test_开头
“”“测试加法”“”
s = 1 + 2
assert s == 3, f'断言失败, {s} != 3' # 断言
或
class TestAdd: # 测试类
def test_add_01(self): # 测试方法
“”“测试加法01”“”
s = 1 + 2
assert s == 3, f'断言失败, {s} != 3' # 断言
- 运行测试用例
$ pytest test_calc.py
或
if __name__ == '__main__':
import pytest
pytest.main([__file__]) # pytest测试当前文件
测试准备及清理
Pytest中可以使用不同范围的setup/teardown方法进行测试准备及清理。
def setup_module():
print('测试模块准备')
def teardown_module():
print('测试模块清理')
def setup_function():
print('测试函数准备')
def teardown_function():
print('测试函数清理')
def test_add(): # 测试函数
"""测试加法"""
s = 1 + 2
assert s == 3, f'断言失败, {s} != 3' # 断言
或
class TestAdd: # 测试类
def setup_class(self):
print('测试类准备')
def teardown_class(self):
print('测试类清理')
def setup_method(self):
print('测试方法准备')
def teardown_method(self):
print('测试方法清理')
def test_add_01(self): # 测试方法
"""测试加法01"""
s = 1 + 2
assert s == 3, f'断言失败, {s} != 3' # 断言
参数化(数据驱动)
Pytest中可以@pytest.mark.paramitrize()
装饰器将每条数据变成一条用例。
@pytest.mark.parametrize('a', [1,2,3]) # 参数变量名,数据列表
def test_data(a): # 需要添加和上面同名的参数
"""测试参数化数据"""
print('a =', a)
支持多个参数变量,也支持为每个数据添加自定义说明(id)。
data = [(1,2,3), (0,0,0), (-1,2,1)]
ids = ['test1+2', 'test0+0', 'test-1+2’]
@pytest.mark.parametrize(‘a,b,excepted’, data, ids=ids) # 多个参数变量名写到同一字符串里
def test_add(a,b,excepted): # 需要添加和上面同名的参数
“”“测试加法"""
s = a + b
assert s == excepted, f'断言失败, {s} != {excepted}' # 断言
跳过及期望失败
Pytest中可以@pytest.mark.skip()
或@pytest.mark.skipif()
装饰器来跳过或根据条件跳过用例。
无条件跳过
@pytest.mark.skip('待实现')
def test_sub():
pass
或根据条件跳过
from platform import platform
@pytest.mark.skipif(platform == 'Windows', reason='不支持Windows')
def test_linux_cmd():
pass
也可以使用@pytest.mark.xfail()
来期望用例失败。
@pytest.mark.xfail(reason='期望失败,1+1!=3')
def test_add():
assert 1 + 1 == 3
Pytest测试框架的命令行参数
用例挑选相关命令行参数
- pytest <目录或文件路径1> <目录或文件路径2> :运行指定目录或文件中所有的测试用例,支持指定多个路径。
- pytest <文件路径::测试函数名>:运行指定测试函数
- pytest <文件路径::测试类名>:运行指定测试类中所有用例
- pytest <文件路径::测试类::测试方法名>:运行指定测试类中指定测试方法
- pytest –k=<正则表达式>:指定测试类/测试函数名称匹配规则,筛选指定用例。
- pytest –m=<标签>:指定标签筛选用例,支持and、or、not,例如 –m 'api not web'
Pytest内置makers标记
在命令行使用pytest –markers可以查看所有可使用(包含用户已注册)标记。
@pytest.mark.filterwarnings(warning):过滤指定警告
@pytest.mark.skip(reason=None):无条件跳过用例个
@pytest.mark.skipif(condition, ..., *, reason=...):根据条件跳过用例
@pytest.mark.xfail(condition, ..., *, reason=..., run=True, raises=None, strict=xfail_strict):根据条件期望用例失败
@pytest.mark.parametrize(argnames, argvalues):参数化数据驱动
@pytest.mark.usefixtures(fixturename1, fixturename2, ...):标记引用(依赖)某些Fixture函数
报告(命令行显示)相关命令行参数
- --verbosity=<数值>:指定显示详细级别,0-1,默认0。
- -v / --verbose:提高显示详细级别
- -q / --quiet:安静模式
- -s:不捕获用例输出(用例print信息直接输出到命令行)
- --capture=<捕获方法>:选择捕获用例输出到方法,支持fd、sys、no、tee-sys
- --no-header:不显示头部信息(测试开始环境信息)
- --no-summary:不显示运行总结
- -r <字符>:显示额外测试状态的详情信息,支持f-失败用例,E-异常用例,s-跳过用例,x-预期失败用例,X-非预期成功用例,p-通过用例,P-通过用例带输出,a-所有成功用例,A-全部用例
- --duration=<数字>:显示运行最慢的前几条用例
- --duration-min: 设置运行时间最小阈值(大于该值视为运行较慢)
缓存(重复运行)相关命令行参数
- --lf / --last-failed:运行上次失败的用例
- --ff / --failed-first:先运行上次失败的用例,再运行其他用例
- --nf / --last-failed:先运行新的测试用例文件,在运行其他用例
- --cache-clear:清理之前测试的缓存结果
- ... ...
用例收集相关命令行参数
- --log-level=LEVEL:设置日志等级
- --log-format=LOG_FORMAT:设置日志格式
- --log-date-format=LOG_DATE_FORMAT:设置日志日期格式
- --log-cli-level=LOG_CLI_LEVEL:设置命令行日志登记
- --log-cli-format=LOG_CLI_FORMAT:设置命令行日志格式
- --log-cli-date-format=LOG_CLI_DATE_FORMAT:设置命令行日志日期格式
- --log-file=LOG_FILE:设置日志文件路径
- --log-file-level=LOG_FILE_LEVEL:设置日志文件等级
- --log-file-format=LOG_FILE_FORMAT:设置日志文件日志格式
- --log-file-date-format=LOG_FILE_DATE_FORMAT:设置日志文件日期格式
- --log-auto-indent=LOG_AUTO_INDENT:设置多行文本日志缩进,支持true|on, false|off 或整数值。
- --log-disable=LOGGER_DISABLE:根据名称禁用某个logger,支持指定多次。
Pytest测试框架的配置项
pytest.ini文件[pytest]中常用配置项如下
配置项 | 说明 |
---|---|
addopts | 默认额外的参数; |
cache_dir | 缓存目录,用于缓存上次执行失败的用例; |
markers | 注册的自定义标记及说明; |
norecursedirs | 不遍历的目录; |
testpaths | 测试目录; |
python_files | 测试脚本匹配规则,默认test_开头的py文件视为用例,如果有的测试脚本不是以test_开头,可以配置为pythonfiles = *; |
python_class | 测试类匹配规则,默认Test开头的类视为测试类; |
python_functions | 测试函数及测试方法匹配规则,默认test_开头的函数及方法视为测试用例; |
console_output_style | 命令行输出样式,支持classic、progress、counts三种样式; |
filterwarnings | 过滤的警告; |
xfail_strict | 启用时,标记为xfail的用例通过时状态为Fail,否则为XPassed。 |
日志相关配置如下
配置项 | 说明 |
---|---|
log_print | 用例失败时是否显示相关日志; |
log_cli | 配置为ture时开启命令行日志; |
log_file | 配置日志文件路径,每次覆盖,不支持追加模式;v |
log_cli_level/log_file_level | 配置输出到命令行及文件的日志等级; |
log_cli_format/log_file_format | 配置输出到命令行及文件的日志格式; |
log_cli_date_format/log_file_date_format | 配置日志的日期格式。 |
Pytest测试框架进阶
Pytest测试框架的扩展(钩子)机制
钩子函数(Hooks)是一种特殊的函数,可以在执行过程中“顺带”执行一些自定义的操作。
Pytest测试框架提供了许多钩子函数,可以在测试过程的不同阶段执行自定义的操作。
- 初始化
- 添加钩子pytest_addhooks
- 添加参数pytest_addoption
- 注册插件pytest_plugin_registered
- 启动
- 启动命令行主流程pytest_cmdline_main
- 生成配置 pytest_configure
- 启动运行会话 pytest_sessionstart
- 收集用例
- 判断是否忽略pytest_ignore_collect
- 创建待收集文件对象pytest_collect_file
- 创建待收集模块对象pytest_pycollect_makemodule
- 开始收集当前模块用例pytest_collectstart
- 创建当前模块收集报告对象pytest_make_collect_report
- 创建待收集用例对象pytest_pycollect_makeitem
- 生成用例pytest_generate_tests
- 收集用例完毕pytest_itemcollected
- 生成收集报告pytest_collectreport
- 调整收集的用例pytest_collection_modifyitems
- 用例收集完毕pytest_report_collectionfinish
- 执行测试
- 记录警告信息pytest_warning_recorded
- 启动执行循环pytest_runtestloop
- 开始执行当前用例pytest_runtest_protocol
- 开始记录日志pytest_runtest_logstart
- 开始运行测试准备pytest_runtest_setup
- 执行某个测试准备方法pytest_fixture_setup
- 执行测试用例函数pytest_pyfunc_call
- 开始运行测试清理pytest_runtest_teardown
- 执行某个测试清理方法pytest_fixture_post_finalizer
- 停止记录日志pytest_runtest_logfinish
- 结束
- 测试会话结束 pytest_sessionfinish
- 生成命令行测试总结 pytest_terminal_summary
- 恢复配置 pytest_unconfigure
初始化时的钩子函数
- pytest_addhooks(pluginmanager):添加新的钩子函数
- pytest_addoption(parser, pluginmanager):添加命令行参数及ini配置项
- pytest_configure(config):初始化配置
- pytest_unconfigure(config):恢复配置,退出测试进程前执行
- pytest_sessionstart(session):测试会话开始
- pytest_sessionfinish(session, exitstatus):测试会话结束
- pytest_plugin_registered(plugin, manager):注册了新插件
示例-添加自定义参数及配置项
def pytest_addoption(parser): # 初始化是的钩子放假,用来增加命令行参数
# 添加命令行参数--send_email,执行时带上该参数则视为ture
parser.addoption("--send_email", action="store_true", help="send email after test")
# 添加邮件主题、收件人、正文配置项
parser.addini('email_subject', help='test report email subject')
parser.addini('email_receivers', help='test report email receivers')
parser.addini('email_body', help='test report email body')
示例-修改配置-组装绝对路径
from datetime import datetime
def pytest_configure(config): # 初始化是的配置方法
log_file = config.getini('log_file') # 如果配置文件配置了log_file
if log_file:
# 使用当前时间格式化后log_file名称,替换原有的配置
# 如果配置的是不带时间格式的如log_file=last_run.log,格式化后不变,
# 如果配置log_file=%Y%m%d%H%M%S.log,格式后成当前时间
config.option.log_file = datetime.now().strftime(log_file)
启动时的钩子函数
- pytest_load_initial_conftests(early_config, parser, args):调用初始化conftest.py文件(仅作为安装插件时)
- pytest_cmdline_parse(pluginmanager, args):初始化config配置,解析命令行参数
- pytest_cmdline_main(config):调用执行主命令行操作,启动测试主循环
收集用例时的钩子函数
- pytest_collection(session):开始收集用例
- pytest_ignore_collect(collection_path, path, config):判断是否忽略该目录
- pytest_collect_file(file_path, path, parent):创建待收集文件对象
- pytest_pycollect_makemodule(module_path, path, parent):创建待收集模块
- pytest_pycollect_makeitem(collector, name, obj):创建待收集用例对象
- pytest_generate_tests(metafunc):生成用例
- pytest_make_parametrize_id(config, val, argname):生成参数化用例id
- pytest_collection_modifyitems(session, config, items):调整收集结果
- pytest_collection_finish(session):收集用例结束
示例-调整用例搜集结果-收集用例
import pytest
class TestCollection:
def __init__(self):
self.collected = []
def pytest_collection_modifyitems(self, items):
for item in items:
self.collected.append(item.nodeid)
def get_testcases(testpath):
coll = TestCollection()
pytest.main([testpath, '--collect-only', '-q'], plugins=[coll]) # 指定插件
return coll.collected
get_testcases('./testcases')
执行测试时的钩子函数
- pytest_runtestloop(session):开始执行用例
- pytest_runtest_protocol(item, nextitem):开始执行某条用例
- pytest_runtest_logstart(nodeid, location):开始记录日志
- pytest_runtest_logfinish(nodeid, location):停止记录日志
- pytest_runtest_makereport(item, call):创建报告对象
- pytest_runtest_setup(item):开始运行测试准备方法
- pytest_runtest_call(item):开始调用测试方法
- pytest_pyfunc_call(pyfuncitem):调用测试函数
- pytest_runtest_teardown(item, nextitem):开始执行测试清理方法
报告相关钩子函数
- pytest_collectstart(collector):开始收集某个模块的用例
- pytest_make_collect_report(collector):为当前模块创建收集报告对象
- pytest_itemcollected(item):收集到一条用例
- pytest_collectreport(report):收集当前模块结束
- pytest_deselected(items):排除部分用例
- pytest_report_header(config, start_path, startdir):命令行输出收集信息
- pytest_report_collectionfinish(config, start_path, startdir, items):收集完毕
- pytest_report_teststatus(report, config):报告setup/用例/teardown执行状态
- pytest_report_to_serializable(config, report):序列化报告内容
- pytest_report_from_serializable(config, data):加载序列化报告内容
- pytest_terminal_summary(terminalreporter, exitstatus, config):生成命令行运行总结
- pytest_fixture_setup(fixturedef, request):执行某个测试准备方法
- pytest_fixture_post_finalizer(fixturedef, request):执行某个测试清理方法
- pytest_warning_recorded(warning_message, when, nodeid, location):记录到警告信息
- pytest_runtest_logreport(report):输出setup/用例/teardown日志
- pytest_assertrepr_compare(config, op, left, right):处理断言
- pytest_assertion_pass(item, lineno, orig, expl):处理用例断言通过
示例-命令行总结-运行后额外操作
def pytest_terminal_summary(config): # 生成报告时的命令行最终总结方法
send_email = config.getoption("--send-email")
email_receivers = config.getini('email_receivers').split(',')
if send_email is True and email_receivers:
log_file = config.getoption('log_file')
email_subject = config.getini('email_subject') or 'TestReport'
email_body = config.getini('email_body') or 'Hi'
if email_receivers:
print('发送邮件 ...')
调试/交互相关的钩子函数
- pytest_internalerror(excrepr, excinfo):运行时出现内部错误
- pytest_keyboard_interrupt(excinfo):运行时用户Ctrl+C中断测试
- pytest_exception_interact(node, call, report):引发可交互的异常
- pytest_enter_pdb(config, pdb):进入pdb调试器
- pytest_leave_pdb(config, pdb):退出pdb调试器
Pytest测试框架的Fixture机制
在Pytest测试框架中,Fixture机制是一种用于管理测试用例依赖关系的机制。Fixture机制可以在测试用例执行之前和之后执行一些操作,例如创建测试数据、打开数据库连接、清理测试数据等。
在Pytest中使用@pytest.fixture装饰器装饰一个函数,该函数可以yield或return返回数据、对象或者一个函数。在测试用例中,使用@pytest.mark.usefixtures装饰器来或将Fixture函数名作为参数或使用Fixture。
import pytest
@pytest.fixture()
def user():
return 'admin', '123456'
@pytest.fixture
def login(user): # Fixture函数可以引用(依赖)其他Fixture函数
# 使用的user参数实际为调用user()函数后的返回值
username, password = user
print(f'{username} 登录')
token = 'abcdef'
yield token # yield上为测试准备,yield下为测试清理
print('退出登录’)
def test_add_project(login): # 引用(依赖)login方法
# 使用login参数实际是调用login()函数的返回值即'abcdef'
print('token =', login)
# 也可以标记使用指定Fixture函数,执行其测试准备即清理方法
@pytest.mark.userfixtures('login')
def test_add_project02():
pass # 这种情况下拿不到Fixture函数的返回结果
Fixture生效范围
Fixture函数可以通过scope指定作用范围,Pytest中的Fixture函数支持以下5种范围:
- Session会话级:scope=‘session’,运行一次Pytest算一次会话。运行期间只setup/teardown一次
Package包级:scope=‘pacakge’,对每个包Python包setup/teardown一次 - Module模块级:scope=‘module’,对每个Python脚本setup/teardown一次
- Class级:scope=‘class’,对每个测试类setup/teardown一次
- Function级:scope=‘function’,默认,每个测试函数或测试方法setup/teardown一次。
from selenium import webdriver
# 整个运行过程中仅启动关闭一次浏览器
@pytest.fixture(scope='session')
def driver():
dr = webdriver.Chrome()
yield dr
dr.quit()
Fixture共享及同名覆盖
Fixture函数一般作为公用的辅助方法或全局变量来使用,因此需要在不同用例中都能使用。Pytest框架中使用固定名称的conftest.py文件,来集中管理Fixture函数 。
conftest.py文件同级即下级目录中的所有用例可以无需导入,直接使用conftest.py中的所有Fixture函数。
conftest.py文件在不同的目录中可以有多个(生效范围不同),同时用例文件中也可以编写Fixture函数,当Fixture函数有重名时,采用“就近”原则,离当前用例最近的Fixture函数生效。
@pytest.fixture(scope='session')
def base_url():
return 'http://localhost:3000'
def test_base_url(base_url):
print('base_url =', base_url)
if __name__ == '__main__':
# pip install pytest-base-url
import pytest
pytest.main([__file__, '-sq','--base-url=http://www.xxx.com'])
conftest.py文件的作用
用来编写Fixture函数
编写钩子Hooks函数
导入其所在目录或包的路径(可以确保项目根目录被导入)
conftest.py所在导入规则:
如果conftest.py所在目录没有__init__.py文件,则Pytest自动导入conftest.py所在目录。
如果有则向上找到最上层的包(最上层一个包含__init__.py的目录),并导入包的所在目录,以确保conftest.py可以使用
Fixture返回函数对象
由于Fixture函数不接收普通参数,无法根据用例需要进行特定的测试准备,此时我们一般需要封装一个实用函数并导入使用,也可以把函数定义到Fixture函数的内部,并返回该函数对象,这样用例无需导入便可直接使用功能函数。示例如下:
常规导入模块方式
auth.py
def login(username, password):
# ...
token = 'abcdef'
return token
test_project_api.py
from .auth import login
def test_add_project():
token = login('admin', 'abc123')
# ...
封装成Fixture方式
conftest.py
@pytest.fixture
def login():
def _login(username, password):
# ...
token = ‘abcdef’
return token
return _login
test_project_api.py
def test_add_project(login): # 无需导入直接使用
# 使用的login参数实际是login()函数的调用结果,
# 即内部的_login函数
token = login('admin', 'abc123')
# ...
Pytest测试框架的参数化机制
Pytest提供了三种参数化方式:
使用@pytest.mark.paramitrize()标记进行数据驱动测试
users = [
('admin', '123456'),
('kevin','abc123'),
('lily', 'abcdef')
]
@pytest.mark.parametrize('user,pwd', users)
def test_login(user, pwd):
print('登录', user, pwd)
使用Fixture函数的params参数,进行参数化测试准备
users = [
('admin', '123456'),
('kevin','abc123'),
('lily', 'abcdef')
]
@pytest.fixture(params=users)
def login(request): # request是系统内置Fixture
user, pwd = request.param
print('登录', user, pwd)
def test_add_project(login): """测试不同用户添加项目"""
# ...
使用钩子函数pytest_generate_tests(),动态成多条用例
* conftest.py*
def pytest_addoption(parser): # 添加命令行参数
parser.addoption("--filepath", action="append",default=[], help="run file list")
def pytest_generate_tests(metafunc):
if "filepath" in metafunc.fixturenames:
filepaths = metafunc.config.getoption("filepath")
metafunc.parametrize("filepath",filepaths)
test_a.py
def test_a(filepath):
print('test_a', filepath)
Pytest测试框架的收集机制
在Pytest测试框架中,收集机制是一种用于自动收集测试用例的机制。Pytest测试框架会自动搜索指定目录下的测试文件,并收集其中的测试用例。
以下是Pytest测试框架的收集机制的一些常用用法:
默认收集规则
Pytest测试框架的默认收集规则是搜索以test_开头或以_test结尾的文件,并收集其中的测试用例。例如,test_sum.py、sum_test.py、test_sum.py::test_sum等文件和测试用例都会被收集。
根据正则匹配收集用例
可以使用pytest命令的-k选项来指定收集指定目录下的测试文件。例如,可以使用以下命令来收集tests目录下的所有测试文件:
$ pytest -k tests
在运行上面的命令后,Pytest测试框架会自动搜索tests目录下的测试文件,并收集其中的测试用例。
自定义收集规则
可以使用pytest_collection_modifyitems
钩子函数来自定义收集规则。
例如,可以使用以下代码来自定义收集规则:
def pytest_collection_modifyitems(items):
for item in items:
if "slow" in item.keywords:
item.add_marker(pytest.mark.slow)
在上面的代码中,使用pytest_collection_modifyitems
钩子函数来修改测试用例列表。如果测试用例的关键字中包含slow,则为该测试用例添加@pytest.mark.slow
标记。
Pytest测试框架高级应用
Pytest插件开发
- 新建一个项目,项目中新建一个包例如pytest_data_file,包含空白的__init__.py文件和一个plugin.py文件。
- 在plugin.py中使用添加自动参数及Fixture函数。
- 编辑项目中的setup.py文件,在setup函数中指定入口entry_points为pytest11,例如:
entry_points={
'pytest11': [
'pytest-data-file = pytest_data_file.plugin',
]
}
以下是一个自定义标记插件开发的示例,为用例添加owner归宿人标记,并可以根据—owner筛选用例。
示例插件安装脚本setup.py代码
from setuptools import setup, find_packages
setup(
name='pytest-data-file',
version='0.1',
author="Han Zhichao",
author_email='superhin@126.com',
description='Fixture "data" and for test from yaml file',
license="MIT license",
url='https://github.com/hanzhichao/pytest-data-file',
include_package_data=True,
packages=find_packages(include=['pytest_data_file']),
zip_safe=True,
setup_requires=['pytest-runner'],
install_requires=['pytest','pytest-runner','pyyaml'],
entry_points={
'pytest11': [
'pytest-data-file = pytest_data_file.plugin',
]
}
)
示例插件核心代码
import pytest
def pytest_configure(config):
config.addinivalue_line(
"markers",
"owner: Mark tests with owner and run tests by use --owner")
def pytest_addoption(parser):
parser.addoption('--owner', action='append', help='Run tests which mark the same owners')
@pytest.fixture
def owner(config):
"""获取当前指定的owner"""
return config.getoption('--owner’)
def pytest_collection_modifyitems(session, config, items):
selected_owners = config.getoption('--owner')
print(selected_owners)
if selected_owners == None:
return
deselected_items = set()
for item in items:
for marker in item.iter_markers('owner'):
[marker_owner] = marker.args
if marker_owner not in selected_owners:
deselected_items.add(item)
selected_items = [item for item in items if item not in deselected_items]
items[:] = selected_items
config.hook.pytest_deselected(items=list(deselected_items))
练习
- 发布一款Pytest插件
- 基于某款测试框架(pytest, unittest, qtaf..) 编写一个简单的用例测试平台(基于文件系统的测试平台)
- 用例列表页面,可以选择调试用例,可以选择一批用例保存为一个测试计划
- 用例计划列表,可以选择执行某个测试计划,生成测试报告
- 测试报告页面