python3 pytest (1) - 基本介绍
1 前言
pytest,作为一款测试框架,并没有继续模仿junit层次分明的工作模式,以至于读完官网文档都感觉是懵的
- 虽然没有明确定义setup/teardown方法,却有其独特的手段实现相同的功能
- 流程控制建立在function的基础上,粒度或许更细,但并不容易上手
- fixture功能过于强大,强大到你都不知道如何使用它
- 各种插件,还支持自定义
2 Fixtures
不同的unittest中的fixture,pytest中的fixture功能更强大,不仅能实现setup/teardown,还能实现参数化
2-1 setup/teardown
通过指定fixture的scope属性,可以实现session, module, class, function级别的控制;使用yield
或者request.addfinalizer
,可以在一个方法内实现setup 和 teardown两种行为
yield
vs request.addfinalizer
- addfinalizer可以指定多个方法
- 即使setup阶段失败,addfinalizer定义的行为依然会执行
assert 0
是为了输出日志
import pytest
@pytest.fixture(scope="session")
def session_scope():
print("session scope setup")
yield
print("session scope teardown")
@pytest.fixture(scope="module")
def module_scope():
print("module scope setup")
yield
print("module scope teardown")
@pytest.fixture(scope="function")
def function_scope(request):
print("function setup")
request.addfinalizer(lambda : print("function teardown"))
@pytest.mark.usefixtures("session_scope", "module_scope", "function_scope")
class TestClass(object):
def test_1(self):
print("run test_1")
assert 0
def test_2(self):
print("run test_2")
assert 0
if __name__ == "__main__":
pytest.main()
==================================
session scope setup
module scope setup
function setup
run test_1
function teardown
function setup
run test_2
function teardown
module scope teardown
session scope teardown
2-2 fixture参数化
指定params属性,实现fixture的参数化,引用该fixture的测试方法将遍历全部参数
import pytest
@pytest.fixture(params=["unittest", "pytest"])
def fix1(request):
print(request.param)
def test_main(fix1):
assert 0
if __name__ == "__main__":
pytest.main(["-v"])
===============================
____ test_main[unittest] ______
unittest
____ test_main[pytest] ______
pytest
2-3 fixture嵌套/组合/覆盖
嵌套指一个fixture引用另一个fixture
import pytest
@pytest.fixture
def fix1():
print("call fix1")
@pytest.fixture
def fix2(fix1):
print("call fix2")
def test_main(fix2):
assert 0
if __name__ == "__main__":
pytest.main()
================================
call fix1
call fix2
组合指多个fixture实现迪卡尔乘积组合
import pytest
@pytest.fixture(params=["unittest", "pytest"])
def fix1(request):
yield request.param
@pytest.fixture(params=["python", "java"])
def fix2(request):
yield request.param
def test_main(fix1, fix2):
print("{} - {}".format(fix1, fix2))
assert 0
if __name__ == "__main__":
pytest.main(["-v"])
============================
unittest - python
unittest - java
pytest - python
pytest - java
覆盖类似于变量的就近原则,本地fixture优先级高于上级或全局
这里就不举例子,可以自行尝试
2-4 Request参数
通过request参数,可以获取大量信息,包括config、fixturename,以及module、cls、function等
import pytest
@pytest.fixture
def fix1(request):
for item in dir(request):
if item.startswith("_"):
continue
print("{:18} => {}".format(item, getattr(request, item)))
def test_1(fix1):
assert 0
if __name__ == "__main__":
pytest.main(["-v"])
====================================
addfinalizer => <bound method SubRequest.addfinalizer of <SubRequest 'fix1' for <Function 'test_1'>>>
applymarker => <bound method FixtureRequest.applymarker of <SubRequest 'fix1' for <Function 'test_1'>>>
cached_setup => <bound method FixtureRequest.cached_setup of <SubRequest 'fix1' for <Function 'test_1'>>>
cls => None
config => <_pytest.config.Config object at 0x000002A19C0B8588>
fixturename => fix1
fixturenames => ['fix1', 'request']
fspath => E:\code\python\lab\_logging\test_pytest.py
funcargnames => ['fix1', 'request']
function => <function test_1 at 0x000002A19C227400>
getfixturevalue => <bound method FixtureRequest.getfixturevalue of <SubRequest 'fix1' for <Function 'test_1'>>>
getfuncargvalue => <bound method FixtureRequest.getfuncargvalue of <SubRequest 'fix1' for <Function 'test_1'>>>
instance => None
keywords => <NodeKeywords for node <Function 'test_1'>>
module => <module 'test_pytest' from 'E:\\code\\python\\lab\\_logging\\test_pytest.py'>
node => <Function 'test_1'>
param_index => 0
raiseerror => <bound method FixtureRequest.raiseerror of <SubRequest 'fix1' for <Function 'test_1'>>>
scope => function
session => <Session '_logging'>
比如读取module, class中的属性
import pytest
module_var = "module var "
@pytest.fixture
def fix1(request):
print(getattr(request.module, "module_var"))
print(getattr(request.cls, "class_var"))
class TestClass(object):
class_var = "class var"
def test_1(self, fix1):
assert 0
if __name__ == "__main__":
pytest.main(["-v"])
======================================
module var
class var
3 Mark
3-1 数据驱动
import pytest
@pytest.mark.parametrize(("a", "b", "expected"), [
[1, 2, 3],
[10, 11, 21],
[1, 1, 1],
])
def test_1(a, b, expected):
assert a + b == expected
if __name__ == "__main__":
pytest.main(["-v"])
==================================
test_pytest.py::test_1[1-2-3] PASSED [ 33%]
test_pytest.py::test_1[10-11-21] PASSED [ 66%]
test_pytest.py::test_1[1-1-1] FAILED [100%]
3-2 用例标记
自定义用例标签,可以指定运行,类似于group的功能
import pytest
@pytest.mark.hehe
def test_1():
assert 0
@pytest.mark.haha
def test_2():
assert 0
if __name__ == "__main__":
pytest.main(["-m", "haha"])
======================================
//只运行test_2
4 pytest_generate_tests
借用官网描述
Sometimes you may want to implement your own parametrization scheme or implement some dynamism for determining the parameters or scope of a fixture. For this, you can use the
pytest_generate_tests
hook which is called when collecting a test function. Through the passed in metafunc object you can inspect the requesting test context and, most importantly, you can call metafunc.parametrize() to cause parametrization.
定制加载外部数据,此处可以根据cls名称读取数据文件(txt, csv等),并将结果添加到parametrize方法内
import pytest
def pytest_generate_tests(metafunc):
if metafunc.cls == TestClass:
func_args = metafunc.cls.datas[metafunc.function.__name__]
keys = sorted(func_args[0])
metafunc.parametrize(keys, [[func_arg[key] for key in keys] for func_arg in func_args])
class TestClass(object):
datas = {
"test_1": [
{"k1": 1, "k2": 1, "ret": 2},
],
"test_2": [
{"num1": 11, "num2": 12},
{"num1": 0, "num2":1}
]
}
def test_1(self, k1, k2, ret):
assert k1 + k2 == ret
def test_2(self, num1, num2):
assert num1 > num2
if __name__ == "__main__":
pytest.main(["-v"])
==============================
test_pytest.py::TestClass::test_1[1-1-2] PASSED [ 33%]
test_pytest.py::TestClass::test_2[11-12] FAILED [ 66%]
test_pytest.py::TestClass::test_2[0-1] FAILED [100%]
类似的hook方法还有很多,具体可以参考hookspec.py
5 monkeypatch
当待测试对象依赖于网络、数据库时,通过monkeypath可以略过与第三方的交互,直接指定期望的结果,即实现mock操作
import pytest
import requests
def visit(url):
r = requests.get(url=url)
return r
def test_1(monkeypatch):
monkeypatch.setattr(requests, name="get", value=lambda url: "Hello")
status = visit("https://www.baidu.com")
assert status == "Hello"
if __name__ == "__main__":
pytest.main(["-v"])
其实monkeypatch就是一个fixture,内置的还有tmpdir, reports, unittest, runner等,具体可以查看_pytest
目录