pytest学习总结
官方pytest文档:Full pytest documentation — pytest documentation
一、pytest以及辅助插件的安装
1、pytest安装
pip install pytest
2、辅助插件及介绍
pytest-html(生成htm报告的插件) pytest-xdist (多线程运行的插件) pytest-ordering (改变用例的执行顺序的插件) pytest-rerunfailres (失败用例重跑的插件) allure-pytest (生成美观自定义的allure报告)pytest test_*.py --alluredir=result + allure serve result
allure generate --alluredir=result -o report/ --clean
pytest-rerunfailures (失败重跑插件)
pytest-repeat (重复执行用例的插件)
pytest-assume (assume多重断言,断言失败后边的代码会继续执行)
//插件太多,可通过pip install -r pludin.txt一次性下载完成
例如:plugin.txt中内容如下
pytest-html
pytest-xdist
终端运行:pip install -r pugin.txt即可下载这两个插件
二、pytest运行
测试套件默认规则:测试文件必须以test_*开头,测试类名字必须是Test*开头,测试方法名字必须是test_*开头,否则运行时会找不到测试套件
pytest命令的参数:
-vs 输出详细信息。输出调试信息。如: pytest -vs
-n 多线程运行。( 前提安装插件: pytest-xdist)如: pytest -VS -n=2 --reruns 失败重跑(前提安装插件: pytest-rerunfailres )如: pytest --reruns=2 raise Exception()抛出异常,try except解决异常。 -x 出现一个用例失败则停止测试。如: pytest -x --maxfail 出现几个失败才终止,如: pytest --maxfail=2 --html 生成html的测试报告(前提安装插件: pytest-html) , 如: pytest --html=report/report.html -k 运行测试用例名称中包含某个字符串的测试用例。 -m 只测试被标记的用例
--strict-markers 未在pytest.ini配置文件注册的标记mark都会引发报错
--reruns n (安装pytest-rerunfailures插件),失败重跑n次,最大运行次数n pytest --reruns 5
--reruns-delay n )(pytest-rerunfailures插件),pytest --rerun 2 --reruns-delay 5 失败重跑之间间隔5s
--count 重复执行测试,需安装pytest-repeat,使用:pytest --count=5或者pytest --count 5
重复执行所有测试用例5次,主要结合-x使用,测试偶发bug,运行直到执行失败
还有@pytest.mark.repeat(n)可对测试类、方法使用
有两种执行方式:
1)命令行运行:pytest -vs -x
2)主函数运行:
if __name__ == "__main__": pytest.main(["-vs”,“-x"])
指定运行:
pytest test_*.py //运行指定的test_*.py ,多个py文件分号隔开 pytest testcase/ //运行指定的testcase目录下所有测试文件 pytest -k "cs” //测试包含有“cs”的测试类和方法 pytest testcase/test_*.py::Test_lei::test_* //可具体指定某个方法
测试执行结果:
- 退出code 0: 收集并成功通过所有测试用例
- 退出code 1: 收集并运行了测试,部分测试用例执行失败
- 退出code 2: 测试执行被用户中断
- 退出code 3: 执行测试中发生内部错误
- 退出code 4: pytest命令行使用错误
- 退出code 5: 没有收集到测试用例
全局配置文件pytest.ini,一般建在项目根目录下
注意:
1)这个配置文件的名称不能改
2)编码格式是ANSI,写入中文后自动变为GB2312 中文简体
3)pytest.main()会带上其中配置的参数
pytest.ini配置文件内容解释,实际中根据需求可选择性添加配置。
[pytest]
//pytest参数配置 addopts = -VS
//需要执行用例模块目录 testpaths =./testcases
//可更改测试文件、类、方法的默认规则,一般不做更改 python_files = test_*.py python_classes = Test* python_functions = test_*
//标记用例,一般用于冒烟测试,以下的smoke是标签名,”冒烟测试“是描述,使用
markers=
smoke:"冒烟测试"
flow:"流水"
//与@pytest.mark.xfail()的strict一样,为False,意外pass的结果显示xpass,为True,则显示Failed
xfail_strict = False
//控制台日志输出控制器,为True输出,为False关闭
log_cli = True
//用来加速收集用例,去掉不要谝历的目录
norecursedirs = * (目录)
断言类型
1)assert正常断言
def test_001(self):
assert var==3 //判断var等于3
assert var in ‘ab’ //判断字符串‘ab’包含var
assert var is False //判断var 为假
assert var not True //判断var不为真
assert var //判断var为真
2)pytest.raise()异常断言
def test_002(self): with pytest.raises(ZeroDivisionError): 1/0
3)pytest.warns()警示断言
暂没有合适例子
临时目录
1)项目运行过程中,总会产生一些临时文件,pytest提供了tmp_path来创建临时目录。
tmp_path是一个pathlib/pathlib2.Path
对象。以下是测试使用方法的示例如:
# test_tmp_path.py文件内容 import os CONTENT = u"content" def test_create_file(tmp_path): d = tmp_path / "sub" d.mkdir() p = d / "hello.txt" p.write_text(CONTENT) assert p.read_text() == CONTENT assert len(list(tmp_path.iterdir())) == 1 assert 0
测试结果如下:除了assert0以外,其他都断言成功 _____________________________ test_create_file _____________________________ tmpdir = local('PYTEST_TMPDIR/test_create_file0') def test_create_file(tmpdir): p = tmpdir.mkdir("sub").join("hello.txt") p.write("content") assert p.read() == "content" assert len(tmpdir.listdir()) == 1 > assert 0 E assert 0
2)tmpdir_factory
四、前置和后置
前置和后置方法如下:
- 模块级别:setup_module、teardown_module
- 函数级别:setup_function、teardown_function,不在类中的方法
- 类级别:setup_class、teardown_class
- 方法级别:setup_method、teardown_method
- 方法细化级别:setup、teardown
def setup_module(): print("=====整个.py模块开始前只执行一次:打开浏览器=====") def teardown_module(): print("=====整个.py模块结束后只执行一次:关闭浏览器=====") def setup_function(): print("===每个函数级别用例开始前都执行setup_function===") def teardown_function(): print("===每个函数级别用例结束后都执行teardown_function====") def test_one(): print("one") class TestCase(): def setup_class(self): print("====整个测试类开始前只执行一次setup_class====") def teardown_class(self): print("====整个测试类结束后只执行一次teardown_class====") def setup_method(self): print("==类里面每个用例执行前都会执行setup_method==") def teardown_method(self): print("==类里面每个用例结束后都会执行teardown_method==") def setup(self): print("=类里面每个用例执行前都会执行setup=") def teardown(self): print("=类里面每个用例结束后都会执行teardown=") def test_three(self): print("two")
五、fixtrue实现前后置
@pytest.fixtrue(scope="xxx")和yeild实现前置和后置
1)参数scope的五个范围级别:fuction(默认)、class、module、package、session,实例化顺序级别从右至左
2)类调用fixtrue固件只能通过@pytest.mark.usefixtrues("aaa","bbb"),aaa和bbb均为函数名
3)方法只能通过参数调用,如def aaa(bbb),aaa为调用函数,bbb为固件函数名
具体效果如下:
//不调用则不显示 @pytest.fixture() def ttt(): print("fuction-setup4") @pytest.fixture() def aaa(): print("fuction-setup1") @pytest.fixture() def bbb(): print("fuction-setup2") @pytest.fixture(scope="class") def ccc(): print("setup") //调用程序前执行 yield //后面的的方法体相当于后置 print("teardown") //调用程序后执行 //类调用fuction范围级别固件bbb,相当于类里面的每个方法都调用bbb @pytest.mark.usefixtures("bbb") //类调用class级别固件ccc,只在调用的类前执行一次 @pytest.mark.usefixtures("ccc") class Test_study: def test_ddd(self,aaa): //先bbb输出,再aaa输出 print("AAAAAA") @pytest.mark.smoke def test_eee(self): print("BBBBBB") -------------------结果如下-------------------------------- kuadi/test_kuadi_login.py::Test_study::test_ddd setup fuction-setup2 fuction-setup1 AAAAAA PASSED kuadi/test_kuadi_login.py::Test_study::test_eee fuction-setup2 BBBBBB PASSEDteardown
4)不规范和错误的调用
@pytest.fixture()
def aaa():
print("fuction-setup1")
# @pytest.mark.usefixtures("ccc") //fixtrue函数之间如此调用无效,只能通过参数调用其他古剑的函数名
@pytest.fixture(aaa)
def bbb():
print("fuction-setup2")
----------------------------------
@pytest.fixture(scope="class") def ccc(): print("setup") yield print("teardown") class Test_study: @pytest.mark.usefixtures("bbb") //调用无效 def test_ddd(self): print("AAAAAA") def test_eee(self,ccc): //参数调用class级别的固件相当于类调用 print("BBBBBB") ----------------------结果如下--------------- kuadi/test_kuadi_login.py::Test_study::test_ddd fuction-setup2 AAAAAA PASSED kuadi/test_kuadi_login.py::Test_study::test_eee setup BBBBBB PASSEDteardown
5)yield和addfinalizer函数的使用
//yield配合fixtrue实现前后置方法 @pytest.fixture(scope="class") def ccc(): print("setup") yield print("teardown") //with配合yield,等待测试完成后,自动关闭连接 @pytest.fixture(scope="module") def smtp_connection(): with smtplib.SMTP("smtp.gmail.com", 587, timeout=5) as smtp_connection: yield smtp_connection //通过后置操作在测试完成后关闭浏览器连接 @pytest.fixture(scope="module") def test_addfinalizer(request): # 前置操作setup print("==再次打开浏览器==") test = "test_addfinalizer" def fin(): # 后置操作teardown print("==再次关闭浏览器==") request.addfinalizer(fin) # 返回前置操作的变量 return test
六、conftest文件
conftest文件注意事项:
- pytest会默认读取conftest.py里面的所有fixture,所以不用导入fixtrue
- conftest.py 文件名称是固定的,不能改动
- conftest.py中fixtrue只对同一个package下的所有测试用例生效,其他目录中引入会报错not found
- 不同目录可以有自己的conftest.py,一个项目中可以有多个conftest.py
- 下级目录的conftest中fixtrue可以覆盖重写上级目录中同名的fixtrue,且就近生效
- 测试用例文件中不需要手动import conftest.py,pytest会自动查找
七、参数化
@pytest.mark.parametrize()
官方文档:How to parametrize fixtures and test functions — pytest documentation
用法如下:
//测试方法和函数使用
@pytest.mark.parametrize("test_input,expected", [("3+5", 8), ("2+4", 6), ("6*9", 42)]) def test_eval(test_input, expected): assert eval(test_input) == expected
//测试类使用 @pytest.mark.parametrize("n,expected", [(1, 2), (3, 4)]) class TestClass: def test_simple_case(self, n, expected): assert n + 1 == expected def test_weird_simple_case(self, n, expected): assert (n * 1) + 1 == expected
//模块全局使用 pytestmark = pytest.mark.parametrize("n,expected", [(1, 2), (3, 4)]) class TestClass: def test_simple_case(self, n, expected): assert n + 1 == expected
//mark标记使用 @pytest.mark.parametrize( "test_input,expected", [("3+5", 8), ("2+4", 6), pytest.param("6*9", 42, marks=pytest.mark.xfail)], ) def test_eval(test_input, expected): assert eval(test_input) == expected
//叠加使用 @pytest.mark.parametrize("x", [0, 1]) @pytest.mark.parametrize("y", [2, 3]) def test_foo(x, y): print({x},{y}) 结果按顺序:0,2/1,2/0.3/1,3
@pytest.fixtrue()
第一个例子
# content of conftest.py import pytest import smtplib @pytest.fixture(scope="module", params=["smtp.gmail.com", "mail.python.org"]) def smtp_connection(request): smtp_connection = smtplib.SMTP(request.param, 587, timeout=5) yield smtp_connection print("finalizing {}".format(smtp_connection)) smtp_connection.close()
//第二个例子
@pytest.fixture(params=[0, 1], ids=["spam", "ham"]) def a(request): return request.param def test_a(a): pass def idfn(fixture_value): if fixture_value == 0: return "eggs" else: return None @pytest.fixture(params=[0, 1], ids=idfn) def b(request): return request.param def test_b(b): pass 结果-------------------------------------------------
@pytest.fixtrue带标记
@pytest.fixture(params=[0, 1, pytest.param(2, marks=pytest.mark.skip)]) def data_set(request): return request.param def test_data(data_set): pass
fixtrue中使用fixtrue:smtp_connection()是第一个用例中fixtrue
class App: def __init__(self, smtp_connection): self.smtp_connection = smtp_connection @pytest.fixture(scope="module") def app(smtp_connection): return App(smtp_connection) def test_smtp_connection_exists(app): assert app.smtp_connection
按fix true实例自动对测试进行分组
@pytest.fixture(scope="module", params=["mod1", "mod2"]) def modarg(request): param = request.param print(" SETUP modarg", param) yield param print(" TEARDOWN modarg", param) @pytest.fixture(scope="function", params=[1, 2]) def otherarg(request): param = request.param print(" SETUP otherarg", param) yield param print(" TEARDOWN otherarg", param) def test_0(otherarg): print(" RUN test0 with otherarg", otherarg) def test_1(modarg): print(" RUN test1 with modarg", modarg) def test_2(otherarg, modarg): print(" RUN test2 with otherarg {} and modarg {}".format(otherarg, modarg)) --------------------------------结果------------------------------- collecting ... collected 8 items test_module.py::test_0[1] SETUP otherarg 1 RUN test0 with otherarg 1 PASSED TEARDOWN otherarg 1 test_module.py::test_0[2] SETUP otherarg 2 RUN test0 with otherarg 2 PASSED TEARDOWN otherarg 2 test_module.py::test_1[mod1] SETUP modarg mod1 RUN test1 with modarg mod1 PASSED test_module.py::test_2[mod1-1] SETUP otherarg 1 RUN test2 with otherarg 1 and modarg mod1 PASSED TEARDOWN otherarg 1 test_module.py::test_2[mod1-2] SETUP otherarg 2 RUN test2 with otherarg 2 and modarg mod1 PASSED TEARDOWN otherarg 2 test_module.py::test_1[mod2] TEARDOWN modarg mod1 SETUP modarg mod2 RUN test1 with modarg mod2 PASSED test_module.py::test_2[mod2-1] SETUP otherarg 1 RUN test2 with otherarg 1 and modarg mod2 PASSED TEARDOWN otherarg 1 test_module.py::test_2[mod2-2] SETUP otherarg 2 RUN test2 with otherarg 2 and modarg mod2 PASSED TEARDOWN otherarg 2 TEARDOWN modarg mod2
测试功能不需要直接访问fixtrue时,如建一个临时目录或者文件供测试使用,结合@pytest.markusefixtrues()
# content of conftest.py import os import tempfile import pytest @pytest.fixture def cleandir(): with tempfile.TemporaryDirectory() as newpath: old_cwd = os.getcwd() os.chdir(newpath) yield os.chdir(old_cwd) ------------------------------------ # content of test_setenv.py import os import pytest @pytest.mark.usefixtures("cleandir") class TestDirectoryInit: def test_cwd_starts_empty(self): assert os.listdir(os.getcwd()) == [] with open("myfile", "w") as f: f.write("hello") def test_cwd_again_starts_empty(self): assert os.listdir(os.getcwd()) == []
//其他用法 @pytest.mark.usefixtures("cleandir", "anotherfixture") 指定多个fix true def test(): pass //使用pytestmark在测试模块级别指定夹具的用途 pytestmark = pytest.mark.usefixtures("cleandir") //将项目中所有测试所需的夹具放入ini文件中 [pytest] usefixtures = cleandir
结合@pytest.mark.parametrize()使用,fixtrue作测试数据处理功能,如:factory
@pytest.fixture(scope="function") def input_user(request): user = request.param print("登录账户:%s" % user) return user @pytest.fixture(scope="function") def input_psw(request): psw = request.param print("登录密码:%s" % psw) return psw name = ["name1", "name2"] pwd = ["pwd1", "pwd2"] @pytest.mark.parametrize("input_user", name, indirect=True) @pytest.mark.parametrize("input_psw", pwd, indirect=True) def test_more_fixture(input_user, input_psw): print("fixture返回的内容:", input_user, input_psw)
fixtrue会覆盖同名的fixtrue
tests/ __init__.py conftest.py # content of tests/conftest.py import pytest @pytest.fixture(params=['one', 'two', 'three']) //最初的参数化fixtrue def parametrized_username(request): return request.param @pytest.fixture //最初的非参数化fixtrue def non_parametrized_username(request): return 'username' test_something.py # content of tests/test_something.py import pytest @pytest.fixture //非参数化fixtrue覆盖参数化fixtrue def parametrized_username(): return 'overridden-username'
//参数化fixtrue覆盖非参数化fixtrue @pytest.fixture(params=['one', 'two', 'three']) def non_parametrized_username(request): return request.param def test_username(parametrized_username): assert parametrized_username == 'overridden-username' def test_parametrized_username(non_parametrized_username): assert non_parametrized_username in ['one', 'two', 'three'] test_something_else.py # content of tests/test_something_else.py def test_username(parametrized_username): assert parametrized_username in ['one', 'two', 'three'] def test_username(non_parametrized_username): assert non_parametrized_username == 'username'
直接参数化的fixtrue覆盖非参数化fixtrue
//直接参数化也可以覆盖fixtrue
tests/ __init__.py conftest.py # content of tests/conftest.py import pytest @pytest.fixture def username(): return 'username' test_something.py # content of tests/test_something.py import pytest @pytest.mark.parametrize('username', ['directly-overridden-username']) def test_username(username): assert username == 'directly-overridden-username'
八、其他装饰器
官方API Reference — pytest documentation
1、@pytest.mark.xfail()、pytest.xfail
@pytest.mark.xfail(condition,*,reason=none,raises=none,run=True,strict=False)
1)raises:none表示默认匹配所有异常错误,值为type(Exception),只能是异常值
2)condition:boolea or str类型,就是有条件执行,只跳过部分特殊用例时使用
3)run:为true,该怎么样就怎么样,为False,则该测试套件不会运行,且始终标为xfail
4)reson:原因说明
5)strict:为False,用例通过显示xpass,用例失败显示xfail,为True,用例失败显示xfail,用例意外通过则显示fail
函数、方法、类执行前使用@pytest.mark.xfail(),预期失败
对方法使用:
//raises参数为空或者与所报的错误匹配时结果是xfail,此结果是预期失败,说明用例通过 @pytest.mark.xfail() def test_002(self): print("账户修改") raise Exception //raises参数与所报的错误不匹配时结果是failed @pytest.mark.xfail(raises=ZeroDivisionError) def test_004(self): print("账户修改") raise Exception --------------------结果------------------------ test_interface.py::Test_kuaidi::test_002 账户修改 XFAIL test_interface.py::Test_kuaidi::test_004 账户修改 FAILED
对类使用:
@pytest.mark.xfail(raises=ZeroDivisionError) class Test_kuaidi: def test_002(self): //不匹配的用例显示failed print("账户修改") raise Exception def test_004(self): //xpass,意外通过 print("账户修改") # raise Exception def test_005(self): //匹配的用例显示xfail 1/0 ------------------------结果展示--------------- test_interface.py::Test_kuaidi::test_002 账户修改 FAILED test_interface.py::Test_kuaidi::test_004 账户修改 XPASS test_interface.py::Test_kuaidi::test_005 XFAIL
pytest.xfail(reason=none),主要使用来对已知错误和功能缺失使用的,使用例子:
def test_aaa(): pytest.xfail(reason="功能未做") ---------结果------------------------- test_interface.py::test_aaa XFAIL (功能未做)
2、@pytest.mark.skip()、@pytest.mark.skipif()、pytest.skip、pytest.importorskip
都是跳过用例,区别:
@pytest.mark.skip(reason=None):reason跳过的原因,不影响程序运行
@pytest.mark.skipif(condition, reason=None):condition参数的值只能是Boolean或者str,有条件跳过
@pytest.mark.skip使用在测试类、测试方法、测试函数上,则测试类、测试方法、测试函数中所有用例都会跳过
@pytest.mark.skipif使用在测试类、测试方法、测试函数上,condition有值则根据条件跳过,没有值和@pytest.mark.skip一样
@pytest.mark.skipif(sys.platform == 'win32', reason="does not run on windows") class TestSkipIf(object): def test_function(self): print("不能在window上运行")
pytest.skip(reason=none,allow_module_level=False,msg)
allow_module_level:默认为False,为True则会允许模块调用,执行时会跳过模块中剩余的用例
def test_aaa(): pytest.skip("功能未做") //注意,pytest.skip(reason="功能未做")会报错 结果:test_interface.py::test_aaa SKIPPED (功能未做) pytest.skip(r"功能未做",allow_module_level=True) 结果:跳过所有剩余用例
pytest.importorskip(modname,minversion=none,reason=none)
作用:缺少导入包,跳过用例测试
参数:modname,导入的模块名;minversion,模块最小版本号
expy = pytest.importorskip("pytest", minversion="7.0",reason="pytest7.0版本没有出来") @expy def test_import(): print("test")
结果:会跳过所有该模块的用例测试
3、@pytest.mark.flaky()
前提安装pytest-rerunfailures,reruns:重跑次数,reruns_delay:重跑间隔时间
禁止:1、不能和@pytest.fixtrue()一起使用
2、该插件与pytest-xdist的looponfail不兼容
3、与核心--pdb标志不兼容
@pytest.mark.flaky(reruns=2,reruns_delay=5) def test_ddd(self): assert False ---------------------执行结果------------------------- kuadi/test_kuadi_login.py::Test_study::test_ddd RERUN kuadi/test_kuadi_login.py::Test_study::test_ddd RERUN kuadi/test_kuadi_login.py::Test_study::test_ddd FAILED