https://docs.pytest.org/en/6.2.x/
1,测试类主函数模式
import pytest
def test_a():
assert True
def test_b():
assert False
if __name__ == "__main__":
import sys
script_name = sys.argv[0]
pytest.main(["-v", script_name])
2,命令行模式
x
pytest
pytest [单测文件名|目录名]
pytest 单测文件名::单测函数名
pytest 单测文件名::单测类名<::单测方法名>
1,--version
查看版本
2,-s
在测试执行期间,任何标准输出,标准错误输出都会被捕获。如果想禁用该行为,需要设置 --capture=no(-s 是其快捷方式)
3,-r
pytest 单独统计和列出 skip 和 xfail 测试。skipped/xfailed 测试的详细信息不会被展示到输出里。使用 -r 选项可以查看测试过程中的细节。
当 -r 选项被指定为如下字符时,将展示额外的测试信息:
比如:
xxxxxxxxxx
pytest -rxXs # show extra info on xfailed, xpassed, and skipped tests
4,--collect-only
只收集测试用例,不执行
5,--fixtures
显式可用的 fixture,不执行用例
6,--maxfail
在发生 N 次失败或错误后退出。比如:
pytest --maxfail=2 example_test.py
7,--pyargs
自动导入包,并使用包目录,执行下面的测试用例
8,-n
用多进程运行测试用例。需要安装 xdist 插件:
xxxxxxxxxx
pip install pytest-xdist
9,--reruns
重新运行失败的测试用例的次数。需要安装 rerunfailures 插件:
xxxxxxxxxx
pip install pytest-rerunfailures
10,--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,不管类里有多少个测试函数:
xxxxxxxxxx
class 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() 函数的签名:
xxxxxxxxxx
def 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 True
2,通过装饰器引用
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 True
3,自动引用
xxxxxxxxxx
import pytest
class 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:
xxxxxxxxxx
pytest -s <单测文件名>::TestFixtureC
4.1,向 test 传递数据
xxxxxxxxxx
import 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:
xxxxxxxxxx
pytest -s <单测文件名>::TestFixtureD
4.2,使用不同的参数多次调用测试用例
xxxxxxxxxx
import 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.py
def test_compute(param1):
assert param1 < 4
增加一个类似下面的测试配置:
xxxxxxxxxx
# content of conftest.py
def 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 < 4
E assert 4 < 4
test_compute.py:4: AssertionError
========================= short test summary info ==========================
FAILED test_compute.py::test_compute[4] - assert 4 < 4
1 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.py
import pytest
from datetime import datetime, timedelta
testdata = [
(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 == expected
def 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.y
cachedir: $PYTHON_PREFIX/.pytest_cache
rootdir: $REGENDOC_TMPDIR
collected 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 进行参数化测试:
xxxxxxxxxx
import pytest
fixture .
def 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-python
serialization via the pickle module.
"""
import shutil
import subprocess
import textwrap
import pytest
pythonlist = ["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.py
sssssssssssssssssssssssssss [100%]
========================= short test summary info ==========================
SKIPPED [9] multipython.py:29: 'python3.5' not found
SKIPPED [9] multipython.py:29: 'python3.6' not found
SKIPPED [9] multipython.py:29: 'python3.7' not found
27 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 sys
import pytest
if 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.py
import mymodule
minversion = pytest.mark.skipif(
mymodule.__versioninfo__ < (1, 1), reason="at least mymodule-1.1 required"
)
def test_function():
...
可以在其它模块中导入并重用:
# test_myothermodule.py
from test_mymodule import minversion
def 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.py
pytestmark = pytest.mark.skipif(...)
当多个 skipif 装饰器被应用到同一个测试函数时,如果任意一个跳过条件为 True,那么该函数将被跳过。
XFail:标记测试函数为预期失败
使用 xfail 标记意味着期望测试失败:
mark.xfail .
def 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 之类的标记应用到独立的用例:
xxxxxxxxxx
import 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-cov
2,运行测试用例、统计覆盖率、生成报告:
xxxxxxxxxx
pytest --cov=<执行期间将要统计覆盖率的包名或路径名,可以指定多次> --cov-report=<生成的报告类型> ...