本文从六个部分总结pytest
库的用法和使用场景
pytest在test*.py
或者 *test.py
文件中; 寻找以 test
开头或结尾的函数,以Test
开头的类里面的 以 test
开头或结尾的方法,将这些作为测试用例。
所以需要满足以下
1.文件名以 test
开头或结尾;
2.函数/方法以test
开头;
3.class类名以Test
开头, 且不能有__init__
方法
ps: 基于PEP8规定一般我们的测试用例都是 test_xx, 类则以Test_; 即带下划线的,但是要注意的是不要下划线也是可以运行的!
pytest.main()
作为测试用例的入口
pytest.main(["./"])
pytest.main(["./sub_dir"]) # 运行sub_dir目录
pytest.main(["./sub_dir/test_xx.py"]) # 运行指定模块
pytest.main(["./sub_dir/test_xx.py::test_func"]) # 运行指定函数用例
pytest.main(["./sub_dir/test_xx.py::TestClass::test_method"]) # 运行指定类用例下的方法
命令行
略
(参下 - 以下用例都使用的命令行)目录树
tests
├── test_class.py
class TestClass:
def test_pass():
assert 1
def test_faild():
assert 0
└── test_func.py
def test_pass():
assert 1
def test_faild():
assert 0
项目目录下会建立一个tests目录,里面存放单元测试用例,如上所示 ,两个文件 test_class.py
是测试类,test_func.py
是测试函数
在如上目录下运行pytest, pytest会在当前目录及递归子目录下按照上述的规则运行测试用例;
如果只是想收集测试用例,查看收集到哪些测试用例可以查看--collect-only
命令选项
[python -m] pytest --collect-only
输出, 可以看到collected 3 items
即该命令收集到3个测试用例
============================================================= test session starts ==============================================================
platform darwin -- Python 3.7.3, pytest-4.6.5, py-1.11.0, pluggy-0.13.1
rootdir: /Users/huangxiaonan/PycharmProjects/future
plugins: anyio-3.5.0
collected 3 items
========================================================= no tests ran in 0.15 seconds =========================================================
测试用例基础工具assert
contest.py
自定义pytest_assertrepr_compare
该函数有三个参数
class Foo:
def __init__(self, val):
self.val = val
def __eq__(self, other):
return self.val == other.val
def pytest_assertrepr_compare(op, left, right):
if isinstance(left, Foo) and isinstance(right, Foo) and op == "==":
return [
"Comparing Foo instances:",
" vals: {} != {}".format(left.val, right.val),
]
test_assert.pyfrom conftest import Foo
def test_compare():
f1 = Foo(1)
f2 = Foo(2)
assert f1 == f2
输出============================================================= test session starts ==============================================================
platform darwin -- Python 3.7.3, pytest-4.6.5, py-1.11.0, pluggy-0.13.1 -- /Library/Developer/CommandLineTools/usr/bin/python3
cachedir: .pytest_cache
rootdir: /Users/PycharmProjects/future/tests, inifile: pytest.ini
plugins: anyio-3.5.0
collected 1 item
tests/test_assert.py::test_compare FAILED
=================================================================== FAILURES ===================================================================
_________________________________________________________________ test_compare _________________________________________________________________
def test_compare():
f1 = Foo(1)
f2 = Foo(2)
> assert f1 == f2
E assert Comparing Foo instances:
E vals: 1 != 2
tests/test_assert.py:7: AssertionError
=========================================================== 1 failed in 0.05 seconds ===========================================================
pytest.raises()
捕获指定异常,以确定异常发生import pytest
def test_raises():
with pytest.raises(TypeError) as e:
raise TypeError("TypeError")
exec_msg = e.value.args[0]
assert exec_msg == 'TypeError'
::
显示指定pytest test__xx.py::test_func
-k
模糊匹配pytest -k a # 调用所有带a字符的测试用例
pytest.mark.自定义标记
装饰器做标记
@pytest.mark.finished
def test_func1():
assert 1 == 1
@pytest.mark.unfinished
def test_func2():
assert 1 != 1
@pytest.mark.success
@pytest.finished
def test_func3():
assert 1 == 1
def pytest_configure(config):
marker_list = ["finished", "unfinished", "success"] # 标签名集合
for markers in marker_list:
config.addinivalue_line("markers", markers)
[pytest]
markers=
finished: finish
error: error
unfinished: unfinished
ps: 如果不注册也可以运行,但是会有 warnning
注意: 标签要用双引号
finished
标签的用例pytest -m "finished" #
运行 test_fun1
test_fun2
pytest -m "finished or unfinished"
运行 test_fun3
pytest -m "finished and success""
SKIPPED
装饰测试函数或者测试类
pytest.mark.skip(reason="beause xxx")
直接跳过pytest.mark.skipif(a>=1, reason="当a>=1执行该测试用例")
满足条件跳过
import pytest
@pytest.mark.skip(reason="no reason, skip")
class TestB:
def test_a(self):
print("------->B test_a")
assert 1
def test_b(self):
print("------->B test_b")
@pytest.mark.skipif(a>=1, reason="当a>=1执行该测试用例")
def test_func2():
assert 1 != 1
pytest tests/test_mark.py -v
)============================================================= test session starts ==============================================================
platform darwin -- Python 3.7.3, pytest-4.6.5, py-1.11.0, pluggy-0.13.1 -- /Library/Developer/CommandLineTools/usr/bin/python3
cachedir: .pytest_cache
rootdir: /Users/huangxiaonan/PycharmProjects/future/tests, inifile: pytest.ini
plugins: anyio-3.5.0
collected 3 items
tests/test_mark.py::TestB::test_a SKIPPED [ 33%]
tests/test_mark.py::TestB::test_b SKIPPED [ 66%]
tests/test_mark.py::test_func2 SKIPPED [100%]
========================================================== 3 skipped in 0.02 seconds ===========================================================
XPASS
可预见的错误,不想skip, 比如某个测试用例是未来式的(版本升级后)
pytest.mark.xfail(version < 2, reason="no supported until version==2")
pytest.mark.parametrize(argnames, argvalues)
@pytest.mark.parametrize('passwd',
['123456',
'abcdefdfs',
'as52345fasdf4'])
def test_passwd_length(passwd):
assert len(passwd) >= 8
============================================================= test session starts ==============================================================
platform darwin -- Python 3.7.3, pytest-4.6.5, py-1.11.0, pluggy-0.13.1 -- /Library/Developer/CommandLineTools/usr/bin/python3
cachedir: .pytest_cache
rootdir: /Users/huangxiaonan/PycharmProjects/future/tests, inifile: pytest.ini
plugins: anyio-3.5.0
collected 3 items
tests/test_params.py::test_passwd_length[123456] FAILED [ 33%]
tests/test_params.py::test_passwd_length[abcdefdfs] PASSED [ 66%]
tests/test_params.py::test_passwd_length[as52345fasdf4] PASSED [100%]
=================================================================== FAILURES ===================================================================
多参数情况@pytest.mark.parametrize('user, passwd', [('jack', 'abcdefgh'), ('tom', 'a123456a')])
常见的就是数据库的初始连接和最后关闭操作
@pytest.fixture()
def postcode():
return '010'
def test_postcode(postcode):
assert postcode == '010'
在生产环境中一般定义在conftest.py
集中管理;pytest.fixture()
装饰的函数此处为 postcode
, 如果想要使用它,需要给测试用例添加同名形参;也可以自定义固件名称pytest.fixture(name="code")
此时 fixture的名称为code
.以yield
分割,预处理在yield
之前,后处理在yield
之后
import pytest
@pytest.fixture(scope="module")
def db_conn():
print(f"mysql conn")
conn = None
yield conn
print(f"mysql close")
del conn
def test_postcode(db_conn):
print("db_conn = ", db_conn)
assert db_conn == None
scope
可接受如下参数
装饰类或者函数用例,完成一些用例的预处理和后处理工作
import pytest
@pytest.fixture(scope="module")
def db_conn():
print(f"mysql conn")
conn = None
yield conn
print(f"mysql close")
del conn
@pytest.fixture(scope="module")
def auth():
print(f"login")
user = None
yield user
print(f"logout")
del user
@pytest.mark.usefixtures("db_conn", "auth")
class TestA:
def test_a(self):
assert 1
def test_b(self):
assert 1
执行python3 -m pytest tests/test_fixture.py -vs
fixture
将以 db_conn -> auth 的顺序(即位置参数的先后顺序)在用例TestA
构建作用域pytest.mark.usefixtures
的方式构建,此时靠近被装饰对象的fixture优先============================================================= test session starts ==============================================================
platform darwin -- Python 3.7.3, pytest-4.6.5, py-1.11.0, pluggy-0.13.1 -- /Library/Developer/CommandLineTools/usr/bin/python3
cachedir: .pytest_cache
rootdir: /Users/PycharmProjects/future/tests, inifile: pytest.ini
plugins: anyio-3.5.0
collected 2 items
tests/test_fixture.py::TestA::test_a mysql conn
login
PASSED
tests/test_fixture.py::TestA::test_b PASSEDlogout
mysql close
=========================================================== 2 passed in 0.06 seconds ===========================================================
fixture参数autouse=True
, 则fixture将自动执行
新建一个conftest.py
文件,添加如下代码,以下代码是官方给出的demo,计算 session 作用域(总测试),及 function 作用域(每个函数/方法)的运行时间
import time
import pytest
DATE_FORMAT = '%Y-%m-%d %H:%M:%S'
@pytest.fixture(scope='session', autouse=True)
def timer_session_scope():
start = time.time()
print('\nstart: {}'.format(time.strftime(DATE_FORMAT, time.localtime(start))))
yield
finished = time.time()
print('finished: {}'.format(time.strftime(DATE_FORMAT, time.localtime(finished))))
print('Total time cost: {:.3f}s'.format(finished - start))
@pytest.fixture(autouse=True)
def timer_function_scope():
start = time.time()
yield
print(' Time cost: {:.3f}s'.format(time.time() - start))
当执行conftest.py
当前目录及递归子目录下的所有用例时将自动执行以上fixture区别于参数化测试, 这部分主要是对固件进行参数化,比如连接两个不同的数据库
固件参数化需要使用 pytest 内置的固件 request
,并通过 request.param
获取参数。
import pytest
@pytest.fixture(params=[
('redis', '6379'),
('mysql', '3306')
])
def param(request):
return request.param
@pytest.fixture(autouse=True)
def db(param):
print('\nSucceed to connect %s:%s' % param)
yield
print('\nSucceed to close %s:%s' % param)
def test_api():
assert 1 == 1
============================================================= test session starts ==============================================================
platform darwin -- Python 3.7.3, pytest-4.6.5, py-1.11.0, pluggy-0.13.1 -- /Library/Developer/CommandLineTools/usr/bin/python3
cachedir: .pytest_cache
rootdir: /Users/PycharmProjects/future/tests, inifile: pytest.ini
plugins: anyio-3.5.0
collected 2 items
tests/test_fixture.py::test_api[param0]
Succeed to connect redis:6379
PASSED
Succeed to close redis:6379
tests/test_fixture.py::test_api[param1]
Succeed to connect elasticsearch:9200
PASSED
Succeed to close elasticsearch:9200
=========================================================== 2 passed in 0.01 seconds ===========================================================
tmpdir
作用域 function
用于临时文件和目录管理,默认会在测试结束时删除
tmpdir.mkdir()
创建目临时目录返回创建的目录句柄,tmpdir.join()
创建临时文件返回创建的文件句柄
def test_tmpdir(tmpdir):
a_dir = tmpdir.mkdir('mytmpdir')
a_file = a_dir.join('tmpfile.txt')
a_file.write('hello, pytest!')
assert a_file.read() == 'hello, pytest!'
tmpdir_factory
作用于所有作用域
@pytest.fixture(scope='module')
def my_tmpdir_factory(tmpdir_factory):
a_dir = tmpdir_factory.mktemp('mytmpdir')
a_file = a_dir.join('tmpfile.txt')
a_file.write('hello, pytest!')
return a_file
pytestconfig
读取命令行参数和配置文件; 等同于request.config
pytest_addoption
用于添加命令行参数def pytest_addoption(parser):
parser.addoption('--host', action='store',
help='host of db')
parser.addoption('--port', action='store', default='8888',
help='port of db')
def test_option1(pytestconfig):
print('host: %s' % pytestconfig.getoption('host'))
print('port: %s' % pytestconfig.getoption('port'))
capsys
临时关闭标准输出stdout
, stderr
import sys
def test_stdout(capsys):
sys.stdout.write("stdout>>")
sys.stderr.write("stderr>>")
out, err = capsys.readouterr()
print(f"capsys stdout={out}")
print(f"capsys stderr={err}")
============================================================= test session starts ==============================================================
platform darwin -- Python 3.7.3, pytest-4.6.5, py-1.11.0, pluggy-0.13.1 -- /Library/Developer/CommandLineTools/usr/bin/python3
cachedir: .pytest_cache
rootdir: /Users/huangxiaonan/PycharmProjects/future/tests, inifile: pytest.ini
plugins: anyio-3.5.0
collected 1 item
tests/test_assert.py::test_stdout capsys stdout=stdout>>
capsys stderr=stderr>>
PASSED
=========================================================== 1 passed in 0.05 seconds ===========================================================
recwarn
捕获程序中的warnnings
告警
code (不引入recwarn)
import warnings
def warn():
warnings.warn('Deprecated function', DeprecationWarning)
def test_warn():
warn()
输出 可以看到有warnnings summary
告警
============================================================= test session starts ==============================================================
platform darwin -- Python 3.7.3, pytest-4.6.5, py-1.11.0, pluggy-0.13.1
rootdir: /Users/huangxiaonan/PycharmProjects/future/tests, inifile: pytest.ini
plugins: anyio-3.5.0
collected 1 item
tests/test_assert.py . [100%]
=============================================================== warnings summary ===============================================================
test_assert.py::test_warn
/Users/huangxiaonan/PycharmProjects/future/tests/test_assert.py:5: DeprecationWarning: Deprecated function
warnings.warn('Deprecated function', DeprecationWarning)
-- Docs: https://docs.pytest.org/en/latest/warnings.html
===================================================== 1 passed, 1 warnings in 0.05 seconds =====================================================
code 引入recwarn
import warnings
def warn():
warnings.warn('Deprecated function', DeprecationWarning)
def test_warn(recwarn):
warn()
此时将无告警,此时告警对象可在测试用例中通过recwarn.pop()
获取到
code 以下方式也可以捕获warn
def test_warn():
with pytest.warns(None) as warnings:
warn()
monkeypatch
按照理解来说这些函数的作用仅仅是在测试用例的作用域内有效,参见setenv
import os
def test_config_monkeypatch(monkeypatch):
monkeypatch.setenv('HOME', "./")
import os
print(f"monkeypatch: env={os.getenv('HOME')}")
def test_config_monkeypat():
import os
print(f"env={os.getenv('HOME')}")
输出 可以看到monkeypath
给环境变量大的补丁只在定义的测试用例内部有效============================================================= test session starts ==============================================================
platform darwin -- Python 3.7.3, pytest-4.6.5, py-1.11.0, pluggy-0.13.1 -- /Library/Developer/CommandLineTools/usr/bin/python3
cachedir: .pytest_cache
rootdir: /Users/huangxiaonan/PycharmProjects/future/tests, inifile: pytest.ini
plugins: anyio-3.5.0
collected 2 items
tests/test_assert.py::test_config_monkeypatch monkeypatch: env=./
PASSED
tests/test_assert.py::test_config_monkeypat env=/Users/huangxiaonan
PASSED
=========================================================== 2 passed in 0.06 seconds ===========================================================
pytest两类配置文件