文章目录
- 前言🥁
- 一、构造测试用例 🏀
- 如何构造🤔
- 如何运行🥱
- 简单目录结构示例🕸
- 二、基础用法🐢
- 使用断言🤨
- 捕获异常🧐
- 指定运行测试用例🐗
- 跳过测试用例 `SKIPPED`💦
- 预见的错误 `XPASS`😅
- 参数化💡
- 三、Fixture🔗
- 简单范例🔔
- 预处理和后处理🙈
- fixture作用域🥅
- pytest.mark.usefixtures🔗
- fixture自动化🐝
- fixture参数化🔧
- 内置fixture🪛
- 四、Hooks
- 五、配置文件⚙️
- 六、一个完整的测试用例应该是什么样的?🤔
前言🥁
本文从六个部分总结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"]) # 运行指定类用例下的方法
- 参数
其实和pytest命令行参数一样,只是将参数按照顺序放到列表中传参给main函数
-
命令行
略
(参下 - 以下用例都使用的命令行)
简单目录结构示例🕸
- 目录树
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
<Module tests/test_class.py>
<Class Test_ABC>
<Function test_a>
<Function test_b>
<Module tests/test_func.py>
<Function test_f>
========================================================= no tests ran in 0.15 seconds =========================================================
二、基础用法🐢
使用断言🤨
测试用例基础工具assert
- 自定义断言
在contest.py
自定义pytest_assertrepr_compare
该函数有三个参数
- op
- left
- right
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.py
from 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'
指定运行测试用例🐗
- 1.命令行
::
显示指定
pytest test__xx.py::test_func
- 2.命令行
-k
模糊匹配
pytest -k a # 调用所有带a字符的测试用例
- 3.
pytest.mark.自定义标记
装饰器做标记
- test_mark.py
@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
- 注册标记到配置
- 方式1: conftest.py
def pytest_configure(config):
marker_list = ["finished", "unfinished", "success"] # 标签名集合
for markers in marker_list:
config.addinivalue_line("markers", markers)
- 方式2: pytest.ini
[pytest]
markers=
finished: finish
error: error
unfinished: unfinished
ps: 如果不注册也可以运行,但是会有 warnning <PytestUnknownMarkWarning>
- 运行方式
注意: 标签要用双引号
- 运行带
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执行该测试用例")
满足条件跳过
- code
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)
- code
@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')])
三、Fixture
常见的就是数据库的初始连接和最后关闭操作
简单范例🔔
- code
@pytest.fixture()
def postcode():
return '010'
def test_postcode(postcode):
assert postcode == '010'
- 在生产环境中一般定义在
conftest.py
集中管理;
可以看到如上的范例中定义了一个 fixture 名称以被pytest.fixture()
装饰的函数此处为postcode
, 如果想要使用它,需要给测试用例添加同名形参;也可以自定义固件名称pytest.fixture(name="code")
此时 fixture的名称为code
.
预处理和后处理🙈
以yield
分割,预处理在yield
之前,后处理在yield
之后
- code
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
fixture作用域🥅
scope
可接受如下参数
- function: 函数级(默认),每个测试函数都会执行一次固件;
- class: 类级别,每个测试类执行一次,所有方法都可以使用;
- module: 模块级,每个模块执行一次,模块内函数和方法都可使用;
- session: 会话级,一次测试只执行一次,所有被找到的函数和方法都可用。
pytest.mark.usefixtures
装饰类或者函数用例,完成一些用例的预处理和后处理工作
- code
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自动化🐝
fixture参数autouse=True
, 则fixture将自动执行
新建一个conftest.py
文件,添加如下代码,以下代码是官方给出的demo,计算 session 作用域(总测试),及 function 作用域(每个函数/方法)的运行时间
- code
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
fixture参数化🔧
区别于参数化测试, 这部分主要是对固件进行参数化,比如连接两个不同的数据库
固件参数化需要使用 pytest 内置的固件 request
,并通过 request.param
获取参数。
- code
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 ===========================================================
内置fixture🪛
- tmpdir
作用域function
用于临时文件和目录管理,默认会在测试结束时删除tmpdir.mkdir()
创建目临时目录返回创建的目录句柄,tmpdir.join()
创建临时文件返回创建的文件句柄
- code
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
作用于所有作用域
- code
@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
- conftest.py定义
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')
- test_config.py 定义
def test_option1(pytestconfig):
print('host: %s' % pytestconfig.getoption('host'))
print('port: %s' % pytestconfig.getoption('port'))
- capsys
临时关闭标准输出stdout
,stderr
- code
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
- setattr(target, name, value, raising=True),设置属性;
- delattr(target, name, raising=True),删除属性;
- setitem(dic, name, value),字典添加元素;
- delitem(dic, name, raising=True),字典删除元素;
- setenv(name, value, prepend=None),设置环境变量;
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 ===========================================================
- delenv(name, raising=True),删除环境变量;
- syspath_prepend(path),添加系统路径;
- chdir(path),切换目录。
四、Hooks
五、配置文件⚙️
pytest两类配置文件
- 1.pytest.ini
- 2.conftest.py
六、一个完整的测试用例应该是什么样的?🤔