python pytest
本文由 简悦 SimpRead 转码, 原文地址 https://www.jianshu.com/p/b825addb4e16
Pytest 学习笔记
记录下 pytest 官方文档的阅读笔记,以便后续参考使用。非完整版,个人理解为主,难免有误,望理解。
安装与快速使用
安装
$ pip install pytest
$ pytest --version
第一个 test
01\test_sample.py
def func(x):
return x + 1
def test_answer():
assert func(3) == 5
运行
# 默认会执行当前目录及子目录的所有test_*.py或*_test.py文件。用例执行成功为.,失败为F
$ pytest
# 静默执行
$ pytest -q 01\test_sample.py
# 调试方式执行,可以打印print日志等详情信息
$ pytest 01\test_sample.py -s -v
# python模块方式执行
$ python -m pytest 01\test_sample.py
# 执行单个目录下的tests
$ python 01\
test 类包含多个 tests
01\test_class.py
# pytest默认会执行所有test_前缀的函数
class TestClass(object):
def test_one(self):
x = "this"
assert 'h' in x
def test_two(self):
x = 'hello'
assert hasattr(x, 'check')
pytest 常见的 exit codes
Exit code 0 所有 tests 全部通过
Exit code 1 部分 tests 失败了
Exit code 2 用户中止 test 执行
Exit code 3 执行 test 时,内部报错
Exit code 4 pytest 命令使用姿势不对
Exit code 5 无 tests 可执行
pytest 常见帮助选项
$ pytest --version # 显示版本信息
$ pytest --fixtures # 显示内置可用的函数参数
$ pytest -h | --help # 显示帮助信息
$ pytest -x # 第一个失败时即停止
$ pytest --maxfail=2 # 两个失败后即停止
pytest fixtures(明确的、模块化的、可扩展的)
- fixtures 由明确的命名,可以通过测试函数、模块、类或整个项目激活
- fixtures 以模块化的方式实现,因为每个名称会触发一个 fixtures 函数,同时函数本身也可以使用其它 fixtures
- fixtures 管理从简单的单元到复杂的功能测试,允许参数化 fixtures 和根据配置和组件进行测试或通过函数、类、模块或整个 test 会话范围重用 fixtures
Fixtures 作为函数参数
测试函数可以接收 fixture 对象作为输入参数,使用 @pytest.fixture
test_smtpsimple.py
import pytest
@pytest.fixture
def smtp_connection():
import smtplib
return smtplib.SMTP(host='smtp.qq.com',port=587, timeout=5)
def test_ehlo(smtp_connection):
response, msg = smtp_connection.ehlo()
assert response == 250
assert 0
$ pytest 01\test_smtpsimple.py
============================= test session starts =============================
platform win32 -- Python 3.6.4, pytest-3.7.4, py-1.6.0, pluggy-0.7.1
rootdir: D:\projects\python\pytest_notes, inifile:
collected 1 item
01\test_smtpsimple.py F [100%]
================================== FAILURES ===================================
__________________________________ test_ehlo __________________________________
smtp_connection = <smtplib.SMTP object at 0x0000021F3E041828>
def test_ehlo(smtp_connection):
response, msg = smtp_connection.ehlo()
assert response == 250
> assert 0
E assert 0
01\test_smtpsimple.py:13: AssertionError
========================== 1 failed in 1.45 seconds ===========================
# 测试函数调用smtp_connection参数,而smptlib.SMTP实列由fixture函数创建
Fixtures 依赖注入
Fixtures 允许测试函数非常容易的接收和使用特定的预初始化程序对象,而无需特别去关注 import/setup/cleanup 等细节
config.py:共享 fixture 函数
如果多个测试文件需要用到一个 fixture 函数,则把它写到 conftest.py 文件当中。使用时无需导入这个 fixture 函数,因为 pytest 会自动获取
共享测试数据
- 如果在测试中,需要从文件加载测试数据到 tests,可以使用 fixture 方式加载,pytest 有自动缓存机制
- 另外一种方式是添加测试数据文件到 tests 目录,如使用 pytest-datadir 和 pytest-datafiles 插件
Scope:共享一个 fixture 实列(类、模块或会话)
Scope - module
conftest.py
import pytest
import smtplib
@pytest.fixture(scope='module')
def smtp_connection():
return smtplib.SMTP(host='smtp.qq.com', port=587, timeout=5)
test_module.py
def test_ehlo(smtp_connection):
response, msg = smtp_connection.ehlo()
assert response == 250
assert b'smtp.qq.com' in msg
assert 0
def test_noop(smtp_connection):
response, msg = smtp_connection.noop()
assert response == 250
assert 0
执行
$ pytest 01\test_module.py
============================= test session starts =============================
platform win32 -- Python 3.6.4, pytest-3.7.4, py-1.6.0, pluggy-0.7.1
rootdir: D:\projects\python\pytest_notes, inifile:
collected 2 items
01\test_module.py FF [100%]
================================== FAILURES ===================================
__________________________________ test_ehlo __________________________________
smtp_connection = <smtplib.SMTP object at 0x0000018E14F13780> # 1
def test_ehlo(smtp_connection):
response, msg = smtp_connection.ehlo()
assert response == 250
assert b'smtp.qq.com' in msg
> assert 0
E assert 0
01\test_module.py:7: AssertionError
__________________________________ test_noop __________________________________
smtp_connection = <smtplib.SMTP object at 0x0000018E14F13780> # 2 可以看到1和2的实列对象为同一个
def test_noop(smtp_connection):
response, msg = smtp_connection.noop()
> assert response == 250
E assert 530 == 250
01\test_module.py:12: AssertionError
========================== 2 failed in 1.42 seconds ===========================
Scope - session
import pytest
import smtplib
# 所有tests能使用到它的,都是共享同一个fixture值
@pytest.fixture(scope='session')
def smtp_connection():
return smtplib.SMTP(host='smtp.qq.com', port=587, timeout=5)
Scope - class
import pytest
import smtplib
# 每个test类,都是共享同一个fixture值
@pytest.fixture(scope='class')
def smtp_connection():
return smtplib.SMTP(host='smtp.qq.com', port=587, timeout=5)
高级别的 scope fixtures 第一个实例化
@pytest.fixture(scope="session")
def s1():
pass
@pytest.fixture(scope="module")
def m1():
pass
@pytest.fixture
def f1(tmpdir):
pass
@pytest.fixture
def f2():
pass
def test_foo(f1, m1, f2, s1):
...
- s1: 是最高级别的 fxiture(session)
- m1: 是第二高级别的 fixture(module)
- tmpdir: 是一个 function 的 fixture,依赖 f1
- f1:在 test_foo 列表参数当中,是第一个 function 的 fixture
- f2:在 test_foo 列表参数当中,是最后一个 function 的 fixture
Fixture 结束或执行 teardown 代码
- 当 fixture 超过其 scope 范围,pytest 支持执行 fixture 特定的结束代码
- 使用 yield 替换 return,所有 yield 声明之后的代码都作为 teardown 代码处理
yield 替换 return
conftest.py
import pytest
import smtplib
# print和smtp_connection.close()只会在module范围内最后一个test执行结束后执行,除非中间有异常
@pytest.fixture(scope='module')
def smtp_connection():
smtp_connection = smtplib.SMTP(host='smtp.qq.com', port=587, timeout=5)
yield smtp_connection
print('teardown smtp')
smtp_connection.close()
执行
$ pytest -s -q 01\test_module.py --tb=no
FFteardown smtp
2 failed in 0.96 seconds
with 替换 yield
conftest.py
import pytest
import smtplib
# 使用了with声明,smtp_connection等test执行完后,自动关闭
# 注意:yield之前的setup代码发生了异常,teardown代码将不会被调用
@pytest.fixture(scope='module')
def smtp_connection():
with smtplib.SMTP(host='smtp.qq.com', port=587, timeout=5) as smtp_connection:
yield smtp_connection
使用 addfinalizer 清理
yield 和 addfinalizer 方法类似,但 addfinalizer 有两个不同的地方
可以注册多个 finalizer 函数
不论 setup 代码是否发生异常,均会关闭所有资源
> @pytest.fixture > def equipments(request): > r = [] > for port in ('C1', 'C3', 'C28'): > equip = connect(port) > request.addfinalizer(equip.disconnect) > r.append(equip) > return r > # 假设C28抛出一次,C1和C2将正常被关闭。当然异常发生在finalize函数注册之前,它将不被执行 > > ``` `conftest.py`
import pytest
import smtplib
@pytest.fixture(scope='module')
def smtp_connection(request):
smtp_connection = smtplib.SMTP(host='smtp.qq.com', port=587, timeout=5)
def fin():
print('teardown smtp_connection')
smtp_connection.close()
request.addfinalizer(fin)
return smtp_connection
`执行`
$ pytest -s -q 01\test_module.py --tb=no
FFteardown smtp_connection
2 failed in 0.99 seconds
### Fixture 可以使用 request 对象来管理 测试内容
> Fixture 函数可以接收一个 request 对象
`conftest.py`
import pytest
import smtplib
所有使用fixture的test module,都可以读取一个可选的smtpserver地址
@pytest.fixture(scope='module')
def smtp_connection(request):
server = getattr(request.module, 'smtpserver', 'smtp.qq.com')
smtp_connection = smtplib.SMTP(host=server, port=587, timeout=5)
yield smtp_connection
print('finalizing %s (%s)' % (smtp_connection, server))
smtp_connection.close()
`执行1`
$ pytest -s -q --tb=no smtpserver = 'mail.python.org' # 自动读取并替换fixture默认的值 def test_showhelo(smtp_connection): $ pytest -qq --tb=short 01\test_anothersmtp.py E AssertionError: (250, b'mail.python.org') @pytest.fixture def test_customer_records(make_customer_record): @pytest.fixture def test_customer_records(make_customer_record): import pytest @pytest.fixture(scope='module', def smtp_connection(request): $ pytest -q 01\test_module.py smtp_connection = E assert 0 01\test_module.py:7: AssertionError smtp_connection = E assert 530 == 250 01\test_module.py:12: AssertionError smtp_connection = E AssertionError: assert b'smtp.qq.com' in b'mail.python.org\nPIPELINING\nSIZE 51200000\nETRN\nSTARTTLS\nAUTH DIGEST-MD5 NTLM CRAM-MD5\nENHANCEDST 01\test_module.py:6: AssertionError smtp_connection = E assert 0 01\test_module.py:13: AssertionError import pytest @pytest.fixture(params=[0, 1, pytest.param(2, marks=pytest.mark.skip)]) def test_data(data_set): $ pytest 01\test_fixture_marks.py -v 01/test_fixture_marks.py::test_data[0] PASSED [ 33%] ===================== 2 passed, 1 skipped in 0.14 seconds ===================== def setup_module(module): def teardown_module(module): @classmethod @classmethod def setup_method(self, method): def teardown_method(self, method): def setup_function(function): def teardown_function(function): ```
.FFFfinalizing
FF
5 failed, 1 passed in 2.11 seconds
`test_anothersmtp.py`
assert 0, smtp_connection.helo()
`执行2`
F [100%]
================================== FAILURES ===================================
________________________________ test_showhelo ________________________________
01\test_anothersmtp.py:5: in test_showheloassert 0, smtp_connection.helo()
E assert 0
-------------------------- Captured stdout teardown ---------------------------
finalizing
### Fixture 工厂模式
> 在单个 test 中,fixture 结果需要被多次使用
def make_customer_record():def _make_customer_record(name):
return {
"name": name,
"orders": []
}
return _make_customer_record
customer_1 = make_customer_record("Lisa")
customer_2 = make_customer_record("Mike")
customer_3 = make_customer_record("Meredith")
创建的数据需要工厂管理时,采用这种方式
def make_customer_record():created_records = []
def _make_customer_record(name):
record = models.Customer(name=name, orders=[])
created_records.append(record)
return record
yield _make_customer_record
for record in created_records:
record.destroy()
customer_1 = make_customer_record("Lisa")
customer_2 = make_customer_record("Mike")
customer_3 = make_customer_record("Meredith")
### Fixture 参数
> Fixture 可以参数化,当需要执行多次时
`conftest.py`
import smtplib params=['smtp.qq.com', 'mail.python.org'])
smtp_connection = smtplib.SMTP(host=request.param, port=587, timeout=5)
yield smtp_connection
print('finalizing %s' % (smtp_connection))
smtp_connection.close()
`执行`
FFFF [100%]
================================== FAILURES ===================================
___________________________ test_ehlo[smtp.qq.com] ____________________________def test_ehlo(smtp_connection):
response, msg = smtp_connection.ehlo()
assert response == 250
assert b'smtp.qq.com' in msg
assert 0
___________________________ test_noop[smtp.qq.com] ____________________________def test_noop(smtp_connection):
response, msg = smtp_connection.noop()
assert response == 250
_________________________ test_ehlo[mail.python.org] __________________________def test_ehlo(smtp_connection):
response, msg = smtp_connection.ehlo()
assert response == 250
assert b'smtp.qq.com' in msg
ATUSCODES\n8BITMIME\nDSN\nSMTPUTF8'
---------------------------- Captured stdout setup ----------------------------
finalizing
_________________________ test_noop[mail.python.org] __________________________def test_noop(smtp_connection):
response, msg = smtp_connection.noop()
assert response == 250
assert 0
-------------------------- Captured stdout teardown ---------------------------
finalizing
4 failed in 4.29 seconds
### 标记 fixture 参数
`test_fixture_marks.py`
def data_set(request):return request.param
pass
`执行`
============================= test session starts =============================
platform win32 -- Python 3.6.4, pytest-3.7.4, py-1.6.0, pluggy-0.7.1 -- d:\projects\python\pytest_notes.venv\scripts\python.exe
cachedir: .pytest_cache
rootdir: D:\projects\python\pytest_notes, inifile:
collected 3 items
01/test_fixture_marks.py::test_data[1] PASSED [ 66%]
01/test_fixture_marks.py::test_data[2] SKIPPED [100%]
## 经典的 xunit 风格 setup
### Module 级别的 setup/teardown
> 如果有多个 test 函数或 test 类在一个模块内,可以选择的实现 setup_module 和 teardown_module,一般模块内的所有函数都会调用一次
pass
pass
### Class 级别的 setup/teardown
> 在类内,所有的方法均会调用到
def setup_class(cls):pass
def teardown_class(cls):pass
### Method 和 Function 级别的 setup/teardown
指定方法调用
pass
pass
模块级别内可以直接使用function调用
pass
pass