https://docs.pytest.org/en/6.2.x/
1,测试类主函数模式
import pytestdef test_a(): assert Truedef test_b(): assert Falseif __name__ == "__main__": import sys script_name = sys.argv[0] pytest.main(["-v", script_name])2,命令行模式
x
pytestpytest [单测文件名|目录名]pytest 单测文件名::单测函数名pytest 单测文件名::单测类名<::单测方法名>1,--version
查看版本
2,-s
在测试执行期间,任何标准输出,标准错误输出都会被捕获。如果想禁用该行为,需要设置 --capture=no(-s 是其快捷方式)
3,-r
pytest 单独统计和列出 skip 和 xfail 测试。skipped/xfailed 测试的详细信息不会被展示到输出里。使用 -r 选项可以查看测试过程中的细节。
当 -r 选项被指定为如下字符时,将展示额外的测试信息:
比如:
xxxxxxxxxxpytest -rxXs # show extra info on xfailed, xpassed, and skipped tests4,--collect-only
只收集测试用例,不执行
5,--fixtures
显式可用的 fixture,不执行用例
6,--maxfail
在发生 N 次失败或错误后退出。比如:
pytest --maxfail=2 example_test.py7,--pyargs
自动导入包,并使用包目录,执行下面的测试用例
8,-n
用多进程运行测试用例。需要安装 xdist 插件:
xxxxxxxxxxpip install pytest-xdist9,--reruns
重新运行失败的测试用例的次数。需要安装 rerunfailures 插件:
xxxxxxxxxxpip install pytest-rerunfailures10,--junit-xml
在指定的路径生成 junit-xml 风格的报告文件
pytest 的配置文件通常放在测试目录下,名字为 pytest.ini,命令行运行时会使用该配置文件中的配置:
xxxxxxxxxx[pytest]# 命令行参数addopts = -s# 搜索的文件名python_files = test_*.py# 搜索的类名python_classes = Test*# 搜索的函数名python_functions = test_*# 搜索路径testpaths = tests integration更多详情,请参考:https://docs.pytest.org/en/6.2.x/customize.html。
函数级别
运行于测试方法的始末,即每运行一个测试用例,会运行一次 setup 和 teardown:
class TestClass: def setup(self): print("-----> setup") def teardown(self): print("-----> teardown") def test_method(self): print("-----> test_method")类级别
运行于测试类的始末,即在一个测试内只运行一次 setup_class 和 teardown_class,不管类里有多少个测试函数:
xxxxxxxxxxclass TestClass: def setup_class(self): print("-----> setup_class") def teardown_class(self): print("-----> teardown_class") def test_method_1(self): print("-----> test_method_1") def test_method_2(self): print("-----> test_method_2") def test_method_3(self): print("-----> test_method_3")test fixture 代表执行一个或多个测试用例所需要的准备,以及所有相关的清理操作。比如创建临时的代理数据库、目录,或开启服务进程等。
在 pytest 中,使用 @pytest.fixture 装饰的函数就是一个 fixture。下面是 fixture() 函数的签名:
xxxxxxxxxxdef fixture( fixture_function: Optional[_FixtureFunction] = None, *, scope: "Union[_Scope, Callable[[str, Config], _Scope]]" = "function", params: Optional[Iterable[object]] = None, autouse: bool = False, ids: Optional[ Union[ Iterable[Union[None, str, float, int, bool]], Callable[[Any], Optional[object]], ] ] = None, name: Optional[str] = None,) -> Union[FixtureFunctionMarker, _FixtureFunction]: pass参数的含义如下:
scope:该 fixture 被共享的作用域;其可选的值如下:
params:可选的参数列表,它将导致该 fixture 函数和所有使用它的测试被多次调用。当前参数可以通过 request.param 获取
autouse:如果为 True,pytest 自动地为所有能看到该 fixture 函数的测试激活它。如果为 False(默认值),需要显式地引用才能激活该 fixture
ids:与 params 一一对应的字符串 id 列表,它们是 test id 的一部分。如果没指定,那么 pytest 将从 params 自动生成
name:fixture 的名称
下面通过一些例子进行说明:
1,通过参数引用
import pytest.fixture(scope="function")def my_fixture_1(request): print("-----> before test") yield print("-----> after test")class TestFixture1: """ 通过参数引用 fixture 的测试类 """ def test_a(self, my_fixture_1): # 参数是 fixture 的名字 print("-----> test_a") assert True2,通过装饰器引用
import pytest.fixture(scope="function")def my_fixture(request): print("-----> before test") yield print("-----> after test").mark.usefixtures("my_fixture")class TestFixtureB: def test_b(self): print("-----> test_b") assert True3,自动引用
xxxxxxxxxximport pytestclass TestFixtureC: .fixture(autouse=True) def my_fixture(self): class_name = self.__class__.__name__ print(f"-----> before test in {class_name}") yield print(f"-----> after test in {class_name}") def test_c(self): print("-----> test_c") assert True使用如下命令测试 TestFixtureC:
xxxxxxxxxxpytest -s <单测文件名>::TestFixtureC4.1,向 test 传递数据
xxxxxxxxxximport pytest.fixture()def delivery_data(): print("-----> before delivery_data") yield "deliveried data" print("-----> after delivery_data")class TestFixtureD: def test_d(self, delivery_data): # 通过参数引用 print("-----> test_d") print(f"-----> delivery_data: {delivery_data}") assert True使用如下命令测试 TestFixtureD:
xxxxxxxxxxpytest -s <单测文件名>::TestFixtureD4.2,使用不同的参数多次调用测试用例
xxxxxxxxxximport pytest.fixture(params=[1, 2, 3], ids=["test-1", "test-2", "test=3"]) # ids 可以忽略def delivery_data_multi(request): # 接受封装参数 request print("-----> before delivery_data_multi") yield request.param # 通过 reuqest.param 获取单个参数 print("-----> after delivery_data_multi")class TestFixtureE: def test_d(self, delivery_data_multi): print("-----> test_d") print(f"-----> delivery_data_multi: {delivery_data_multi}") assert True以下内容翻译自:https://docs.pytest.org/en/6.2.x/example/parametrize.html
在 pytest 中可以很容易地参数化测试函数,下面通过一些例子进行说明。
通过命令行生成参数组合
在下面的例子中,通过命令行参数(--all)决定参数的范围,并使用不同的组合参数执行测试:
# content of test_compute.pydef test_compute(param1): assert param1 < 4增加一个类似下面的测试配置:
xxxxxxxxxx# content of conftest.pydef pytest_addoption(parser): parser.addoption("--all", action="store_true", help="run all combinations")def pytest_generate_tests(metafunc): if "param1" in metafunc.fixturenames: if metafunc.config.getoption("all"): end = 5 else: end = 2 metafunc.parametrize("param1", range(end))如果不传递 --all,那么只会执行 2 次测试:
$ pytest -q test_compute.py.. [100%]2 passed in 0.12s下面传递 --all:
xxxxxxxxxx$ pytest -q --all test_compute.py....F [100%]================================= FAILURES =================================_____________________________ test_compute[4] ______________________________param1 = 4 def test_compute(param1):> assert param1 < 4E assert 4 < 4test_compute.py:4: AssertionError========================= short test summary info ==========================FAILED test_compute.py::test_compute[4] - assert 4 < 41 failed, 4 passed in 0.12s如期望所示,运行了 5 次测试,并且最后一次失败了。
用于 test ID 的选项
在参数化测试中,pytest 将为每个值的集合构建字符串类型的 test ID。这些 ID 可以与 -k 选项一起使用,以选择要运行的用例。test ID 也将标识失败的用例。使用 --collect-only 选项运行 pytest 将展示被生成的 ID。
在 test ID 中,数值、字符串、布尔值和 None 将使用它们的常规字符串形式。对于其它对象而言,pytest 将根据参数名生成字符串:
# content of test_time.pyimport pytestfrom datetime import datetime, timedeltatestdata = [ (datetime(2001, 12, 12), datetime(2001, 12, 11), timedelta(1)), (datetime(2001, 12, 11), datetime(2001, 12, 12), timedelta(-1)),].mark.parametrize("a,b,expected", testdata)def test_timedistance_v0(a, b, expected): diff = a - b assert diff == expected.mark.parametrize("a,b,expected", testdata, ids=["forward", "backward"])def test_timedistance_v1(a, b, expected): diff = a - b assert diff == expecteddef idfn(val): if isinstance(val, (datetime,)): # note this wouldn't show any hours/minutes/seconds return val.strftime("%Y%m%d").mark.parametrize("a,b,expected", testdata, ids=idfn)def test_timedistance_v2(a, b, expected): diff = a - b assert diff == expected.mark.parametrize( "a,b,expected", [ pytest.param( datetime(2001, 12, 12), datetime(2001, 12, 11), timedelta(1), id="forward" ), pytest.param( datetime(2001, 12, 11), datetime(2001, 12, 12), timedelta(-1), id="backward" ), ],)def test_timedistance_v3(a, b, expected): diff = a - b assert diff == expected在 test_timedistance_v0 中,我们让 pytest 生成 test ID。
在 test_timedistance_v1 中,我们将 ids 指定为字符串列表。这种方式言简意赅,但是难于维护。
在 test_timedistance_v2 中,我们将 ids 指定为可以生成字符串形式的函数。因此 datetime 值使用 idfn 生成的标签,但是因为没有为 timedelta 对象生成标签,所以它们仍然使用默认的 pytest 形式:
$ pytest test_time.py --collect-only=========================== test session starts ============================platform linux -- Python 3.x.y, pytest-6.x.y, py-1.x.y, pluggy-0.x.ycachedir: $PYTHON_PREFIX/.pytest_cacherootdir: $REGENDOC_TMPDIRcollected 8 items<Module test_time.py> <Function test_timedistance_v0[a0-b0-expected0]> <Function test_timedistance_v0[a1-b1-expected1]> <Function test_timedistance_v1[forward]> <Function test_timedistance_v1[backward]> <Function test_timedistance_v2[20011212-20011211-expected0]> <Function test_timedistance_v2[20011211-20011212-expected1]> <Function test_timedistance_v3[forward]> <Function test_timedistance_v3[backward]>======================== 8 tests collected in 0.12s ========================间接参数化
使用 indirect=True 参数允许使用 fixture 进行参数化测试:
xxxxxxxxxximport pytest.fixturedef fixt(request): return request.param * 3.mark.parametrize("fixt", ["a", "b"], indirect=True)def test_indirect(fixt): assert len(fixt) == 3使用多个 fixture 的间接参数化
在下面的例子中,使用参数化测试测试不同 Python 解释器之间的对象序列化。pytest 将使用不同的参数集合调用 test_basic_objects 函数,该函数有三个参数:
python1:第一个 Python 解释器,运行 pickle 将对象存储到文件python2:第二个 Python 解释器,运行 pickle 从文件加载对象obj:要被存储/加载的对象xxxxxxxxxx"""module containing a parametrized tests testing cross-pythonserialization via the pickle module."""import shutilimport subprocessimport textwrapimport pytestpythonlist = ["python3.5", "python3.6", "python3.7"].fixture(params=pythonlist)def python1(request, tmpdir): picklefile = tmpdir.join("data.pickle") return Python(request.param, picklefile).fixture(params=pythonlist)def python2(request, python1): return Python(request.param, python1.picklefile)class Python: def __init__(self, version, picklefile): self.pythonpath = shutil.which(version) if not self.pythonpath: pytest.skip(f"{version!r} not found") self.picklefile = picklefile def dumps(self, obj): dumpfile = self.picklefile.dirpath("dump.py") dumpfile.write( textwrap.dedent( r""" import pickle f = open({!r}, 'wb') s = pickle.dump({!r}, f, protocol=2) f.close() """.format( str(self.picklefile), obj ) ) ) subprocess.check_call((self.pythonpath, str(dumpfile))) def load_and_is_true(self, expression): loadfile = self.picklefile.dirpath("load.py") loadfile.write( textwrap.dedent( r""" import pickle f = open({!r}, 'rb') obj = pickle.load(f) f.close() res = eval({!r}) if not res: raise SystemExit(1) """.format( str(self.picklefile), expression ) ) ) print(loadfile) subprocess.check_call((self.pythonpath, str(loadfile))).mark.parametrize("obj", [42, {}, {1: 3}])def test_basic_objects(python1, python2, obj): python1.dumps(obj) python2.load_and_is_true(f"obj == {obj}")如果我们没有安装所有 Python 解释器,那么将导致一些测试被跳过;否则会运行所有的组合(3 个解释器 × 3 个解释器 × 3 个将要序列化/反序列化的对象):
$ pytest -rs -q multipython.pysssssssssssssssssssssssssss [100%]========================= short test summary info ==========================SKIPPED [9] multipython.py:29: 'python3.5' not foundSKIPPED [9] multipython.py:29: 'python3.6' not foundSKIPPED [9] multipython.py:29: 'python3.7' not found27 skipped in 0.12s未完待续...
skip 用于在某些条件满足时,跳过测试。比如在非 windows 平台跳过只能在 windows 平台运行的测试。
xfail 意味着期望测试因某种原因失败。比如测试还没实现的特性,或还没修复的 bug。当被期望失败的测试通过时,它是一个 xpass,并且会被展示在测试摘要中。
跳过测试函数
跳过测试函数最简单的方式是使用 skip 装饰器标记它,skip 接受一个可选的 reason:
.mark.skip(reason="no way of currently testing this")def test_the_unknown(): ...另外,也可以在测试执行或设置期间,通过调用 pytest.skip(reason) 函数命令式地跳过测试:
def test_function(): if not valid_config(): pytest.skip("unsupported configuration")当无法在 import 期间计算跳过条件时,命令式方法非常有用。
也可以在模块级别使用 pytest.skip(reason, allow_module_level=True) 跳过整个模块:
import sysimport pytestif not sys.platform.startswith("win"): pytest.skip("skipping windows-only tests", allow_module_level=True)skipif
skipif 可以有条件地跳过测试。在下面的例子中,当在早于 Python3.6 的解释器上运行时,测试函数会被跳过:
import sys.mark.skipif(sys.version_info < (3, 7), reason="requires python3.7 or higher")def test_function(): ...如果在收集期间,条件的计算结果为 True,pytest 将跳过测试函数。当使用 -rs 选项时,被指定的原因将出现在测试概要中。
可以在模块间共享 skipif。比如:
# content of test_mymodule.pyimport mymoduleminversion = pytest.mark.skipif( mymodule.__versioninfo__ < (1, 1), reason="at least mymodule-1.1 required")def test_function(): ...可以在其它模块中导入并重用:
# test_myothermodule.pyfrom test_mymodule import minversiondef test_anotherfunction(): ...另外,也可以使用 condition string 代替 bool 值,但是它们不能方便地在模块间共享,它们被支持的主要原因是为了向后兼容。
跳过类或模块的所有测试方法
可以在类上使用 skipif 标记:
.mark.skipif(sys.platform == "win32", reason="does not run on windows")class TestPosixCalls: def test_function(self): "will not be setup or run under 'win32' platform"如果条件是 True,那么该类中的所有测试方法都会被跳过。
如果想要跳过一个模块中的所有测试方法,可以使用 pytestmark 全局变量:
# test_module.pypytestmark = pytest.mark.skipif(...)当多个 skipif 装饰器被应用到同一个测试函数时,如果任意一个跳过条件为 True,那么该函数将被跳过。
XFail:标记测试函数为预期失败
使用 xfail 标记意味着期望测试失败:
.mark.xfaildef test_function(): ...该测试会被运行,但是当它失败时,不会报告 traceback。终端报告会把它列在 XFAIL 或 XPASS 区域。
另外,也可以在测试函数或它的 setup 函数里,命令式地将测试标记为 XFAIL:
def test_function(): if not valid_config(): pytest.xfail("failing configuration (but should work)") def test_function2(): import slow_module if slow_module.slow_function(): pytest.xfail("slow_module taking too long")注意:pytest.xfail() 调用之后的其它代码不会被执行。
1,condition 参数:
如果测试仅在某个特定条件下,才会被期望为失败,那么可以将该条件作为 xfail 的第一个参数:
xxxxxxxxxx.mark.xfail(sys.platform == "win32", reason="bug in a 3rd party library")def test_function(): ...注意:此时必须传递 reason 参数。
2,reason 参数:
通过 reason 参数可以指定期望失败的动机:
.mark.xfail(reason="known parser issue")def test_function(): ...3,raises 参数:
raise 参数可以是单个异常,也可以是异常元组。当测试函数抛出没在 raises 参数中提及的异常时,它会被报告为常规的失败:
.mark.xfail(raises=RuntimeError)def test_function(): ...4,run 参数:
如果测试函数被标记为 xfail,并且不想运行它,那么需要将 run 参数设置为 False:
.mark.xfail(run=False)def test_function(): ...带参数化的 Skip/xfail
当使用参数化时,可以将 skip、xfail 之类的标记应用到独立的用例:
xxxxxxxxxximport pytest.mark.parametrize( ("n", "expected"), [ (1, 2), pytest.param(1, 0, marks=pytest.mark.xfail), pytest.param(1, 3, marks=pytest.mark.xfail(reason="some bug")), (2, 3), (3, 4), (4, 5), pytest.param( 10, 11, marks=pytest.mark.skipif(sys.version_info >= (3, 0), reason="py2k") ), ],)def test_increment(n, expected): assert n + 1 == expected未完待续...
1,安装 cov 插件:
pip install pytest-cov2,运行测试用例、统计覆盖率、生成报告:
xxxxxxxxxxpytest --cov=<执行期间将要统计覆盖率的包名或路径名,可以指定多次> --cov-report=<生成的报告类型> ...