Pytest框架
简介
- 常用第三方插件
- pytest-selenium(集成selenium)
- pytest-html(完美html测试报告)
- pytest-rerunfailures(失败用例重复执行)
- pytest-xdist(多CPU分发)
- 测试用例的skip和xfail处理
- 可以很好的和jenkins集成
- report框架—allure也支持了pytest
0 命名规范
- 模块以 test_开头,建议 test_+业务名称
- 类的首字母大写,遵循驼峰命名规则
- 方法以 test_开头,遵循 test_+固定位数字+下划线+业务名称,如 test_01_cust_query,test_02_order_list
1 安装
-
安装
pip install -U pytest pytest --version 验证安装
-
pytest文档地址
官方文档:[https://docs.pytest.org/en/latest/contents.html]
-
pytest框架约束
所有的单测文件名都需要满足test_*.py格式或*_test.py格式。
在单测文件中,测试类以Test开头,且不能带有init方法(注意:定义class时,需要以T开头,不然pytest是不会运行该class的)
在单测类中,可以包含一个或多个test_开头的函数。
此时,在执行pytest命令时,会自动从当前目录及子目录中寻找符合上述约束的测试函数来执行。
-
运行
# 运行主函数 if __name__ == '__main__': pytest.main("-s test_abc.py") # 命令行模式 pytest 文件路径/测试文件名 例如:pytest ./test_abc.py
序号 演示命令 命令说明 1 pytest ./ 运行当前目录及子目录下所有用例 2 pytest pytest_ 运行指定目录及子目录下所有用例 3 pytest pytest_/test_one.py 指定模块运行 4 pytest -k test 或 pytest -k “test” 按关键字匹配运行 5 pytest test_one.py::test_func 指定函数运行 6 pytest test_one.py::Testlogin 指定类运行 7 pytest test_one.py::Testlogin::test_01 指定类方法运行 8 pytest --pyargs test_one.py 通过包运行 9 pytest -m me 运行所有用@pytest.mark修饰的用例 -
Pytest Exit Code 含义清单
- Exit code 0 所有用例执行完毕,全部通过
- Exit code 1 所有用例执行完毕,存在Failed的测试用例
- Exit code 2 用户中断了测试的执行
- Exit code 3 测试执行过程发生了内部错误
- Exit code 4 pytest 命令行使用错误
- Exit code 5 未采集到可用测试用例文件
-
获取帮助信息
# 显示可以用的内置函数 pytest --fixtures # 查看帮助信息及配置文件选项 pytest --help
-
控制测试用例执行
pytest -x # 第01次失败,就停止 pytest --maxfail=2 # 出现2个失败就终止 # 指定测试模块 pytest test_mod.py # 指定测试目录 pytest testing/ # 通过关键字表达式过滤执行(跳过运行) pytest -k "MyClass and not method" # 通过node id 指定测试用例 ----- # 运行模块中指定用例 pytest test_mod.py::test_func # 运行模块中指定方法 pytest test_mod.py::TestClass::test_method # 通过标记表达式执行 pytest -m show # 这条命令会执行被装饰器@pytest.mark.show装饰的所有测试用例 ------- # 通过包执行测试 pytest --pyargs pkg.testing # 这条命令会导入包pkg.testing,并使用该包所在的目录,执行下面的用例 # 仅运行上次失败的用例 pytest --lf(--last-failed) # 运行所有用例,但是先运行上一次执行失败的用例 pytest --ff(--failed-first) # 仅收集测试用例,不执行 pytest --collect-only
-
多进程运行cases
# 安装 pip instsall -U pytest-xdist # 运行模式 pytest test_se.py -n NUM # 其中NUM填写并发的进程数
-
失败重新运行cases
# 安装 pip install -U pytest-rerunfailures # 运行模式 pytest test_se.py --reruns NUM # NUM填写重试的次数
-
重复执行
# 安装 pip install pytest-repeat # 重新执行3次 pytest -v -s --count=3 test_pytest.py # 设置重复范围:session,module,class或者function pytest -v -s --count=3 --repeat-scope=session test_pytest.py # 或者在代码中标记 import pytest def calc(a,b): return a + b class TestDemo(): @pytest.mark.repeat(3) def test_answer1(self): assert calc(1, 1) == 2
-
显示print内容
pytest test_se.py -s # 叠加使用 pytest test_se.py -s -n 4 # 同时运行4个进程,打印输出内容 -v --verbose:打印详细日志信息 -q --quiet:打印简略日志信息
-
多条断言
# 安装 pip install pytest-assume # 执行多条断言 # 写法1 pytest.assume(x == y) pytest.assume(True) pytest.assume(False) # 写法2 with assume: assert calc(2, 1) == 4 with assume: assert calc(2, 1) == 3 with assume: assert calc(2, 2) == 3
2 Pytest的setup和teardown函数
setup和teardown主要分为:模块级,类级,功能级,函数级。
# 模块级 在类上面,整个模块只执行一套 # 函数级 与函数对齐,在每个测试函数前后运行,不在类中 # 类级别 在类里面,测试类中调用一套 # 方法级别(功能级别) 与类级别效果一样
存在于测试类内部
-
函数级别setup_method()/teardown_method()
运行于测试方法的始末,即:运行一次测试函数会运行一次setup_method和teardown_method
-
类级别setup_class()/eardown_class()
运行于测试类的始末,即:在一个测试内只运行一次setup_class和teardown_class,不关心测试类内有多少个测试函数。
「setup改setup_method以及teardowm改teardowm_method」
3 Pytest配置文件
pytest的配置文件通常放在测试目录下,名称为pytest.ini,命令行运行时会使用该配置文件中的配置
#配置pytest命令行运行参数
[pytest]
addopts = -s ... # 空格分隔,可添加多个命令行参数 -所有参数均为插件包的参数配置测试搜索的路径
testpaths = ./scripts # 当前目录下的scripts文件夹 -可自定义
#配置测试搜索的文件名称
python_files = test*.py
#当前目录下的scripts文件夹下,以test开头,以.py结尾的所有文件 -可自定义
配置测试搜索的测试类名
python_classes = Test_*
#当前目录下的scripts文件夹下,以test开头,以.py结尾的所有文件中,以Test开头的类 -可自定义
配置测试搜索的测试函数名
python_functions = test_*
#当前目录下的scripts文件夹下,以test开头,以.py结尾的所有文件中,以Test开头的类内,以test_开头的方法 -可自定义
4 Pytest常用插件
插件列表网址:[https://plugincompat.herokuapp.com]
包含很多插件包,大家可依据工作的需求选择使用。
-
前置条件
-
文件路径
Test_App - - test_abc.py - - pytest.ini
-
pytest.ini配置文件内容
[pytest] # 命令行参数 addopts = -s # 搜索文件名 python_files = test_*.py # 搜索的类名 python_classes = Test_* #搜索的函数名 python_functions = test_*
-
-
pip install pytest-ordering(重点)
@pytest.mark.run(order=2) def test_02(self): print("--02--") @pytest.mark.run(order=1) def test_01(self): print("--01--")
-
pip install pytest-xdist(重点)
-
pip install allure-pytest(重点)
-
pip install pytest-dependency(了解)
-
pip install pytest-assume(了解)
-
pip install pytest-random-order(了解)
-
Pytest测试报告(了解)
-
pip install pytest-reportlog(了解)
pytest-HTML是一个插件,pytest用于生成测试结果的HTML报告
# 安装 pip install pytest-html # 运行 pytest --html=用户路径/report.html
# 运行方式 1.修改Test_App/pytest.ini文件,添加报告参数,即:addopts = -s --html=./report.html # -s:输出程序运行信息 # --html=./report.html 在当前目录下生成report.html文件 ️ 若要生成xml文件,可将--html=./report.html 改成 --html=./report.xml 2.命令行进入Test_App目录 3.执行命令: pytest 执行结果: 1.在当前目录会生成assets文件夹和report.html文件
5 pytest的高阶用法1
前置条件
- 文件路径
Test_App
- - test_abc.py
- - pytest.ini
- pyetst.ini配置文件内容:
[pytest]
命令行参数
addopts = -s
搜索文件名
python_files = test*.py
搜索的类名
python_classes = Test*
搜索的函数名
python_functions = test_*
1. pytest之fixture
fixture修饰器来标记固定的工厂函数,在其他函数,模块,类或整个工程调用它时会被激活并优先执行,通常会被用于完成预置处理和重复操作。
方法:fixture(scope=“function”, params=None, autouse=False, ids=None, name=None)
常用参数:
scope: 被标记方法的作用域
"function"(default):作用于每个测试方法,每个test都运行一次
"class":作用于整个模块,每个class的所有test只运行一次
"module":作用于整个模块,每个module的所有test只运行一次
"session":作用于整个session(慎用),每个session(多个文件)只运行一次
params:(list类型)提供参数数据,供调用标记方法的函数使用
autouse:是否自动运行,默认False不运行,设置为True自动运行
2 fixture–通过参数引用(类里面)
class Test_ABC:
@pytest.fixture()
def before(self):
print("------->before")
def test_a(self,before): # ️ test_a方法传入了被fixture标识的函数,以变量的形式
print("------->test_a")
assert 1
if __name__ == '__main__':
pytest.main("-s test_abc.py")
执行结果:
test_abc.py
------->before # 发现before会优先于测试函数运行
------->test_a
.
3 fixture–通过函数引用(类外面)
import pytest
@pytest.fixture() # fixture标记的函数可以应用于测试类外部
def before():
print("------->before")
@pytest.mark.usefixtures("before")
class Test_ABC:
def setup(self):
print("------->setup")
def test_a(self):
print("------->test_a")
assert 1
if __name__ == '__main__':
pytest.main("-s test_abc.py")
执行结果:
test_abc.py
------->before # 发现before会优先于测试类运行
------->setup
------->test_a
.
4 fixture–设置为默认运行
import pytest
@pytest.fixture(autouse=True) # 设置为默认运行
def before():
print("------->before")
class Test_ABC:
def setup(self):
print("------->setup")
def test_a(self):
print("------->test_a")
assert 1
if __name__ == '__main__':
pytest.main("-s test_abc.py")
执行结果:
test_abc.py
------->before # 发现before自动优先于测试类运行
------->setup
------->test_a
.
5 fixture–设置作用域为function
import pytest
@pytest.fixture(scope='function',autouse=True) # 作用域设置为function,自动运行
def before():
print("------->before")
class Test_ABC:
def setup(self):
print("------->setup")
def test_a(self):
print("------->test_a")
assert 1
def test_b(self):
print("------->test_b")
assert 1
if __name__ == '__main__':
pytest.main("-s test_abc.py")
执行结果:
test_abc.py
------->before # 运行第一次
------->setup
------->test_a
.------->before # 运行第二次
------->setup
------->test_b
.
6 fixture–设置作用域为function
import pytest
@pytest.fixture(scope='function',autouse=True) # 作用域设置为function,自动运行
def before():
print("------->before")
class Test_ABC:
def setup(self):
print("------->setup")
def test_a(self):
print("------->test_a")
assert 1
def test_b(self):
print("------->test_b")
assert 1
if __name__ == '__main__':
pytest.main("-s test_abc.py")
执行结果:
test_abc.py
------->before # 运行第一次
------->setup
------->test_a
.------->before # 运行第二次
------->setup
------->test_b
.
7 fixture—设置作用域为class
import pytest
@pytest.fixture(scope='class',autouse=True) # 作用域设置为class,自动运行
def before():
print("------->before")
class Test_ABC:
def setup(self):
print("------->setup")
def test_a(self):
print("------->test_a")
assert 1
def test_b(self):
print("------->test_b")
assert 1
if __name__ == '__main__':
pytest.main("-s test_abc.py")
执行结果:
test_abc.py
------->before # 发现只运行一次
------->setup
------->test_a
.
------->setup
------->test_b
.
8 fixture—返回值
- 示例1
import pytest
@pytest.fixture()
def need_data():
return 2 # 返回数字2
class Test_ABC:
def test_a(self,need_data):
print("------->test_a")
assert need_data != 3 # 拿到返回值做一次断言
if __name__ == '__main__':
pytest.main("-s test_abc.py")
执行结果:
test_abc.py
------->test_a
.
- 示例2
import pytest
@pytest.fixture(params=[1, 2, 3])
def need_data(request): # 传入参数request 系统封装参数
return request.param # 取列表中单个值,默认的取值方式
class Test_ABC:
def test_a(self,need_data):
print("------->test_a")
assert need_data != 3 # 断言need_data不等于3
if __name__ == '__main__':
pytest.main("-s test_abc.py")
执行结果:
# 可以发现结果运行了三次
test_abc.py
1
------->test_a
.
2
------->test_a
.
3
------->test_a
F
9 共享fixture函数:conftest.py
在测试过程中,多个测试文件可能都要调用 fixture 函数,可以将其移动到 conftest.py 文件中。conftest.py 文件中的 fixture 函数不需要在测试函数中导入,可以被 pytest 自动识别,查找顺序从测试类开始,然后是测试模块,然后是 conftest.py 文件,最后是内置插件和第三方插件。
conftest.py
import pytest
@pytest.fixture()
def login():
print("登录")
return 8
测试用例
import pytest
class Test_Demo():
def test_case1(self):
print("\n开始执行测试用例1")
assert 1 + 1 == 2
def test_case2(self, login):
print("\n开始执行测试用例2")
print(login)
assert 2 + login == 10
def test_case3(self):
print("\n开始执行测试用例3")
assert 99 + 1 == 100
if __name__ == '__main__':
pytest.main()
10 yield方法
使用yield关键字可以实现setup/teardown的功能,在yield关键字之前的代码在case之前执行,yield之后的代码在case运行结束后执行
import pytest
@pytest.fixture()
def login():
print("登录")
yield
print("退出登录")
class Test_Demo():
def test_case1(self):
print("\n开始执行测试用例1")
assert 1 + 1 == 2
def test_case2(self, login):
print("\n开始执行测试用例2")
assert 2 + 8 == 10
def test_case3(self):
print("\n开始执行测试用例3")
assert 99 + 1 == 100
if __name__ == '__main__':
pytest.main()
结果
PASSED [ 33%]
开始执行测试用例1
登录
PASSED [ 66%]
开始执行测试用例2
退出登录
PASSED [100%]
开始执行测试用例3
addfinalizer方法暂时省略
6 Pytest高阶用法2
前置条件
- 文件路径
Test_App
- - test_abc.py
- - pytest.ini
- pyetst.ini配置文件内容:
[pytest]
命令行参数
addopts = -s
搜索文件名
python_files = test*.py
搜索的类名
python_classes = Test*
搜索的函数名
python_functions = test_*
1. 跳过测试函数
根据特定的条件,不执行标识的测试函数
方法:
skipif(condition, reason=None)
参数:
condition:跳过的原因
reason:标注原因,必传参数
使用方法:
@pytest.mark.skipif(condition, reason="xxx")
-
示例
import pytest class Test_ABC: def setup_class(self): print("------->setup_class") def teardown_class(self): print("------->teardown_class") def test_a(self): print("------->test_a") assert 1 @pytest.mark.skipif(condition=2>1,reason = "跳过该函数") # 跳过测试函数test_b def test_b(self): print("------->test_b") assert 0 执行结果: test_abc.py ------->setup_class ------->test_a #只执行了函数test_a . ------->teardown_class s # 跳过函数```
2 标记为预期失败的函数
方法:
xfail(condition=None, reason=None, raises=None, run=True, strict=False)
常用参数:
condition:预期失败的条件,必传参数
reason:失败的原因,必传参数
使用方法:
@pytest.mark.xfail(condition, reason="xxx")
import pytest
class Test_ABC:
def setup_class(self):
print("------->setup_class")
def teardown_class(self):
print("------->teardown_class")
def test_a(self):
print("------->test_a")
assert 1
@pytest.mark.xfail(2 > 1, reason="标注为预期失败") # 标记为预期失败函数test_b
def test_b(self):
print("------->test_b")
assert 0
执行结果:
test_abc.py
------->setup_class
------->test_a
.
------->test_b
------->teardown_class
x # 失败标记
3 函数数据参数化
方法:
parametrize(argnames, argvalues, indirect=False, ids=None, scope=None)
常用参数:
argnames: 参数名
argvalues: 参数对应值,类型必须为list
当参数为一个时格式[value]
当参数个数大于一个时,格式为[(param_value1,param_value2.....),(param_value1,param_value2.....)]
使用方法:
@pytest.mark.parametrize(argnames, argvalues) 参数值为N个,测试方法就会运行N次
-
单个参数示例
import pytest class Test_ABC: def setup_class(self): print("------->setup_class") def teardown_class(self): print("------->teardown_class") @pytest.mark.parametrize("a",[3,6]) # a参数被赋予两个值,函数会运行两遍 def test_a(self,a): # 参数必须和parametrize里面的参数一致 print("test data:a=%d"%a) assert a%3 == 0 执行结果: test_abc.py ------->setup_class test data:a=3 # 运行第一次取值a=3 . test data:a=6 # 运行第二次取值a=6 . ------->teardown_class
-
多个参数示例
import pytest class Test_ABC: def setup_class(self): print("------->setup_class") def teardown_class(self): print("------->teardown_class") @pytest.mark.parametrize("a,b",[(1,2),(0,3)]) # 参数a,b均被赋予两个值,函数会运行两遍 def test_a(self,a,b): # 参数必须和parametrize里面的参数一致 print("test data:a=%d,b=%d"%(a,b)) assert a+b == 3 执行结果: test_abc.py ------->setup_class test data:a=1,b=2 # 运行第一次取值 a=1,b=2 . test data:a=0,b=3 # 运行第二次取值 a=0,b=3 . ------->teardown_class
-
函数返回值类型示例:
import pytest def return_test_data(): return [(1,2),(0,3)] class Test_ABC: def setup_class(self): print("------->setup_class") def teardown_class(self): print("------->teardown_class") @pytest.mark.parametrize("a,b",return_test_data()) # 使用函数返回值的形式传入参数值 def test_a(self,a,b): print("test data:a=%d,b=%d"%(a,b)) assert a+b == 3 执行结果: test_abc.py ------->setup_class test data:a=1,b=2 # 运行第一次取值 a=1,b=2 . test data:a=0,b=3 # 运行第二次取值 a=0,b=3 . ------->teardown_class
4 修改Python traceback输出(没懂)
pytest --showlocals # show local variables in tracebacks
pytest -l # show local variables (shortcut)
pytest --tb=auto # (default) 'long' tracebacks for the first and last
# entry, but 'short' style for the other entries
pytest --tb=long # exhaustive, informative traceback formatting
pytest --tb=short # shorter traceback format
pytest --tb=line # only one line per failure
pytest --tb=native # Python standard library formatting
pytest --tb=no # no traceback at all
–full-trace 参数会打印更多的错误输出信息,比参数 --tb=long 还多,即使是 Ctrl+C 触发的错误,也会打印出来
5 执行失败的时候跳转到PDB(没懂)
执行用例的时候,跟参数 --pdb,这样失败的时候,每次遇到失败,会自动跳转到 PDB
pytest --pdb # 每次遇到失败都跳转到 PDB
pytest -x --pdb # 第一次遇到失败就跳转到 PDB,结束测试执行
pytest --pdb --maxfail=3 # 只有前三次失败跳转到 PDB
6 设置断点(没懂)
在用例脚本中加入如下python代码,pytest会自动关闭执行输出的抓取,这里,其他test脚本不会受到影响,带断点的test上一个test正常输出
import pdb; pdb.set_trace()
7 获取用例的执行性能数据
# 获取最慢的10个用例的执行耗时
pytest --durations=10
8 生成JunitXML格式结果文件
pytest --junitxml=path
9 禁用插件
# 例如,关闭 doctest 插件
pytest -p no:doctest
10 从Python代码中调用pytest
pytest.main() # 基本用法
pytest.main(['-x', 'mytestdir']) # 传入配置参数
// 指定自定义的或额外的插件
# content of myinvoke.py
import pytest
class MyPlugin(object):
def pytest_sessionfinish(self):
print("*** test run reporting finishing")
pytest.main(["-qq"], plugins=[MyPlugin()])
11 测试脚本迁移后快速部署包含pytest的virtualenv
cd <repository>
pip install -e .
12 pytest + allure生成测试报告
-
安装java
-
安装allure
-
安装allure-pytest插件
pip install allure-pytest
-
生成Allure报告
pytest [测试文件] -s -q --alluredir=./result
-
查看测试报告
-
直接打开默认浏览器展示报告
allure serve .\result\
-
从结果生成测试报告
- 生成报告
allure generate ./result/ -o ./report/ --clean # 覆盖路径加-clean
- 打开报告
allure open -h 127.0.0.1 -p 8883 ./report/
-
-
allure特性—feature, story, step
@allure.feature("功能名称"):相当于testsuite @allure.story("子功能名称"):相当于testcase @allure.step("步骤"):测试过程中的每个步骤,放在具体逻辑方法中 --allure.step("步骤") 只能以装饰器的形式放在类或者方法上面 --with allure.step:可以放在测试用例方法里面
示例
import pytest import allure @allure.feature("登录") class TestLogin(): @allure.story("登录成功") def test_login_success(self): print("登录成功") pass @allure.story("密码错误") def test_login_failure(self): with allure.step("输入用户名"): print("输入用户名") with allure.step("输入密码"): print("输入密码") print("点击登录") with allure.step("登录失败"): assert '1' == 1 print("登录失败") pass @allure.story("用户名密码错误") def test_login_failure_a(self): print("用户名或者密码错误,登录失败") pass @allure.feature("注册") class TestRegister(): @allure.story("注册成功") def test_register_success(self): print("测试用例:注册成功") pass @allure.story("注册失败") def test_register_failure(self): with allure.step("输入用户名"): print("输入用户名") with allure.step("输入密码"): print("输入密码") with allure.step("再次输入密码"): print("再次输入密码") print("点击注册") with allure.step("注册失败"): assert 1 + 1 == 2 print("注册失败") pass
-
allure特性—link, issue, testcase
可以在测试报告中添加链接、bug地址、测试用例地址。
关联bug时需要再用例执行时添加参数
--allure-link-pattern=issue:"http://www.xxx.com/issure/{}"
示例:
import allure @allure.link("http://www.baidu.com", name="baidu link") def test_with_link(): pass @allure.issue("140","this is a issue") def test_with_issue_link(): pass TEST_CASE_LINK = 'https://github.com' @allure.testcase(TEST_CASE_LINK, 'Test case title') def test_with_testcase_link(): pass
- 执行
pytest test_allure_link_issue.py --allure-link-pattern=issue:"http://www.bugfree.com/issue/{}" --alluredir=./result/3 allure serve ./result/3
-
allure特性—severity
-
可以使用pytest.mark来标记用例(待学习)
@pytest.mark.webtest # 添加标签 @pytest.mark.sec pytest -m "webtest and not sec"
-
通过allure.feature, allure.story来实现
pytest test_feature_story_step.py --allure-features "登录" //只运行登录模块 pytest test_feature_story_step.py --allure-stories "登录成功" //只运行登录成功子模块
-
通过allure.severity按重要性级别来标记
- Blocker级别:阻塞
- Critical级别:严重
- Normal级别:正常
- Minor级别:不太重要
- Trivial级别:不重要
示例test_allure_severity.py
import allure import pytest def test_with_no_severity_label(): pass @allure.severity(allure.severity_level.TRIVIAL) def test_with_trivial_severity(): pass @allure.severity(allure.severity_level.NORMAL) def test_with_normal_severity(): pass @allure.severity(allure.severity_level.NORMAL) class TestclassWithNormalSeverity(object): def test_inside_the_normalseverity_test_class(self): pass @allure.severity(allure.severity_level.CRITICAL) def test_inside_the_normal_severity_test_class_with_overriding_critical_severity(self): pass
运行
pytest .\testcases\test_allure_severity.py --alluredir=./result5/ --allure-severities normal,critical # ------------------------------------- allure serve .\result5
-
-
allure.attach()
可以在报告中附加文本、图片以及html网页,用来补充测试步骤或测试结果,比如错误截图或者关键步骤的截图
@allure.attach("具体文本信息") --附加信息:数据,文本,图片,视频,网页
test_allure_attach.py
import allure import pytest def test_attach_text(): allure.attach("纯文本", attachment_type=allure.attachment_type.TEXT) def test_attach_html(): allure.attach("<body>这是一段htmlbody块</body>", "html页面", attachment_type=allure.attachment_type.HTML) def test_attach_photo(): allure.attach.file("test.jpg", name="图片", attachment_tye=allure.attachment_type.JPG)
运行
pytest test_allure_attach.py --alluredir=./result/5 allure serve ./result/5