- 前言論
- 构造测试用例
- 如何构造樂
- 如何运行辰
- 简单目录结构示例
- 基础用法
- 使用断言廊
- 捕获异常類
- 指定运行测试用例
- 跳过测试用例 `SKIPPED`
- 预见的错误 `XPASS`
- 参数化
- Fixture⚙️
- 简单范例
- 预处理和后处理
- fixture作用域聾
- pytest.mark.usefixtures
- fixture自动化
- fixture参数化
- 内置fixture瀞
本文从两个部分总结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.pyclass 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该函数有三个参数- 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.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.自定义标记 装饰器做标记
- 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
- 方式1: conftest.py
- 运行方式注意: 标签要用双引号
- 运行带 finished标签的用例
pytest -m "finished" #
- 多选 运行 test_fun1 test_fun2
pytest -m "finished or unfinished"
- 多标签用例运行 test_fun3
pytest -m "finished and success""
- 运行带 finished标签的用例
- test_mark.py
装饰测试函数或者测试类
- 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 ===========================================================
- code
可预见的错误,不想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')])
常见的就是数据库的初始连接和最后关闭操作
简单范例- 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
scope可接受如下参数
- function: 函数级(默认),每个测试函数都会执行一次固件;
- class: 类级别,每个测试类执行一次,所有方法都可以使用;
- module: 模块级,每个模块执行一次,模块内函数和方法都可使用;
- session: 会话级,一次测试只执行一次,所有被找到的函数和方法都可用。
装饰类或者函数用例,完成一些用例的预处理和后处理工作
- 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参数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
区别于参数化测试, 这部分主要是对固件进行参数化,比如连接两个不同的数据库
固件参数化需要使用 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 ===========================================================
-
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!'
- code
-
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
- code
-
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'))
- conftest.py定义 pytest_addoption用于添加命令行参数
-
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 ===========================================================
- code
-
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),切换目录。