目录html
fixture
:做为形参使用fixture
:一个典型的依赖注入的实践conftest.py
:共享fixture
实例fixture
实例
fixture
的实例化顺序fixture
的清理操做
fixture
能够访问测试请求的上下文fixture
返回工厂函数fixture
的参数化fixture
中标记用例fixture
使用其它的fixture
fixture
实例fixture
实例fixture
fixture
pytest fixtures
的目的是提供一个固定的基线,使测试能够在此基础上可靠地、重复地执行;对比xUnit
经典的setup/teardown
形式,它在如下方面有了明显的改进:python
fixture
拥有一个明确的名称,经过声明使其可以在函数、类、模块,甚至整个测试会话中被激活使用;fixture
以一种模块化的方式实现。由于每个fixture
的名字都能触发一个fixture函数,而这个函数自己又能调用其它的fixture
;fixture
的管理从简单的单元测试扩展到复杂的功能测试,容许经过配置和组件选项参数化fixture
和测试用例,或者跨功能、类、模块,甚至整个测试会话复用fixture
;此外,pytest
继续支持经典的xUnit
风格的测试。你能够根据本身的喜爱,混合使用两种风格,或者逐渐过渡到新的风格。你也能够从已有的unittest.TestCase
或者nose
项目中执行测试;git
fixture
:做为形参使用测试用例能够接收fixture
的名字做为入参,其实参是对应的fixture
函数的返回值。经过@pytest.fixture
装饰器能够注册一个fixture
;github
咱们来看一个简单的测试模块,它包含一个fixture
和一个使用它的测试用例:数据库
# src/chapter-4/test_smtpsimple.py import pytest @pytest.fixture def smtp_connection(): import smtplib return smtplib.SMTP("smtp.163.com", 25, timeout=5) def test_ehlo(smtp_connection): response, _ = smtp_connection.ehlo() assert response == 250 assert 0 # 为了展现,强制置为失败
这里,test_ehlo
有一个形参smtp_connection
,和上面定义的fixture
函数同名;缓存
执行:bash
$ pipenv run pytest -q src/chapter-4/test_smtpsimple.py F [100%] =============================== FAILURES ================================ _______________________________ test_ehlo _______________________________ smtp_connection = <smtplib.SMTP object at 0x105992d68> def test_ehlo(smtp_connection): response, _ = smtp_connection.ehlo() assert response == 250 > assert 0 # 为了展现,强制置为失败 E assert 0 src/chapter-4/test_smtpsimple.py:35: AssertionError 1 failed in 0.17s
执行的过程以下:服务器
pytest
收集到测试用例test_ehlo
,其有一个形参smtp_connection
,pytest
查找到一个同名的已经注册的fixture
;smtp_connection()
建立一个smtp_connection
实例<smtplib.SMTP object at 0x105992d68>
做为test_ehlo
的实参;test_ehlo(<smtplib.SMTP object at 0x105992d68>)
;若是你不当心拼写出错,或者调用了一个未注册的fixture
,你会获得一个fixture <...> not found
的错误,并告诉你目前全部可用的fixture
,以下:网络
$ pipenv run pytest -q src/chapter-4/test_smtpsimple.py E [100%] ================================ ERRORS ================================= ______________________ ERROR at setup of test_ehlo ______________________ file /Users/yaomeng/Private/Projects/pytest-chinese-doc/src/chapter-4/test_smtpsimple.py, line 32 def test_ehlo(smtp_connectio): E fixture 'smtp_connectio' not found > available fixtures: cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, monkeypatch, pytestconfig, record_property, record_testsuite_property, record_xml_attribute, recwarn, smtp_connection, smtp_connection_package, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory > use 'pytest --fixtures [testpath]' for help on them. /Users/yaomeng/Private/Projects/pytest-chinese-doc/src/chapter-4/test_smtpsimple.py:32 1 error in 0.02s
注意:session
你也可使用以下调用方式:
pytest --fixtures [testpath]它会帮助你显示全部可用的 fixture;
可是,对于
_
开头的fixture
,须要加上-v
选项;
fixture
:一个典型的依赖注入的实践fixture
容许测试用例能够轻松的接收和处理特定的须要预初始化操做的应用对象,而不用过度关心导入/设置/清理的细节;这是一个典型的依赖注入的实践,其中,fixture
扮演者注入者(injector
)的角色,而测试用例扮演者消费者(client
)的角色;
以上一章的例子来讲明:test_ehlo
测试用例须要一个smtp_connection
的链接对象来作测试,它只关心这个链接是否有效和可达,并不关心它的建立过程。smtp_connection
对test_ehlo
来讲,就是一个须要预初始化操做的应用对象,而这个预处理操做是在fixture中完成的;简而言之,test_ehlo
说:“我须要一个SMTP
链接对象。”,而后,pytest
就给了它一个,就这么简单。
关于依赖注入的解释,能够看看Stackflow上这个问题的高票回答如何向一个5岁的孩子解释依赖注入?:
When you go and get things out of the refrigerator for yourself, you can cause problems. You might leave the door open, you might get something Mommy or Daddy doesn't want you to have. You might even be looking for something we don't even have or which has expired.
What you should be doing is stating a need, "I need something to drink with lunch," and then we will make sure you have something when you sit down to eat.
更详细的资料能够看看维基百科Dependency injection;
conftest.py
:共享fixture
实例若是你想在多个测试模块中共享同一个fixture
实例,那么你能够把这个fixture
移动到conftest.py
文件中。在测试模块中你不须要手动的导入它,pytest
会自动发现,fixture
的查找的顺序是:测试类、测试模块、conftest.py
、最后是内置和第三方的插件;
你还能够利用conftest.py
文件的这个特性为每一个目录实现一个本地化的插件;
若是你想多个测试共享一样的测试数据文件,咱们有两个好方法实现这个:
fixture
中,测试中再使用这些fixture
;tests
文件夹中,一些第三方的插件能帮助你管理这方面的测试,例如:pytest-datadir和pytest-datafiles;fixture
实例须要使用到网络接入的fixture
每每依赖于网络的连通性,而且建立过程通常都很是耗时;
咱们来扩展一下上述示例(src/chapter-4/test_smtpsimple.py):在@pytest.fixture
装饰器中添加scope='module'
参数,使每一个测试模块只调用一次smtp_connection
(默认每一个用例都会调用一次),这样模块中的全部测试用例将会共享同一个fixture
实例;其中,scope
参数可能的值都有:function
(默认值)、class
、module
、package
和session
;
首先,咱们把smtp_connection()
提取到conftest.py
文件中:
# src/chapter-4/conftest.py import pytest import smtplib @pytest.fixture(scope='module') def smtp_connection(): return smtplib.SMTP("smtp.163.com", 25, timeout=5)
而后,在相同的目录下,新建一个测试模块test_module.py
,将smtp_connection
做为形参传入每一个测试用例,它们共享同一个smtp_connection()
的返回值:
# src/chapter-4/test_module.py def test_ehlo(smtp_connection): response, _ = smtp_connection.ehlo() assert response == 250 smtp_connection.extra_attr = 'test' assert 0 # 为了展现,强制置为失败 def test_noop(smtp_connection): response, _ = smtp_connection.noop() assert response == 250 assert smtp_connection.extra_attr == 0 # 为了展现,强制置为失败
最后,让咱们来执行这个测试模块:
pipenv run pytest -q src/chapter-4/test_module.py FF [100%] =============================== FAILURES ================================ _______________________________ test_ehlo _______________________________ smtp_connection = <smtplib.SMTP object at 0x107193c50> def test_ehlo(smtp_connection): response, _ = smtp_connection.ehlo() assert response == 250 smtp_connection.extra_attr = 'test' > assert 0 # 为了展现,强制置为失败 E assert 0 src/chapter-4/test_module.py:27: AssertionError _______________________________ test_noop _______________________________ smtp_connection = <smtplib.SMTP object at 0x107193c50> def test_noop(smtp_connection): response, _ = smtp_connection.noop() assert response == 250 > assert smtp_connection.extra_attr == 0 E AssertionError: assert 'test' == 0 E + where 'test' = <smtplib.SMTP object at 0x107193c50>.extra_attr src/chapter-4/test_module.py:33: AssertionError 2 failed in 0.72s
能够看到:
smtp_connection
实例都是<smtplib.SMTP object at 0x107193c50>
,说明smtp_connection
只被调用了一次;test_ehlo
中修改smtp_connection
实例(上述例子中,为smtp_connection
添加extra_attr
属性),也会反映到test_noop
用例中;若是你指望拥有一个会话级别做用域的fixture
,能够简单的将其声明为:
@pytest.fixture(scope='session') def smtp_connection(): return smtplib.SMTP("smtp.163.com", 25, timeout=5)
注意:
pytest
每次只缓存一个fixture
实例,当使用参数化的fixture
时,pytest
可能会在声明的做用域内屡次调用这个fixture
;
package
做用域(实验性的)在 pytest 3.7 的版本中,正式引入了package
做用域。
package
做用域的fixture
会做用于包内的每个测试用例:
首先,咱们在src/chapter-4
目录下建立以下的组织:
chapter-4/ └── package_expr ├── __init__.py ├── test_module1.py └── test_module2.py
而后,在src/chapter-4/conftest.py
中声明一个package
做用域的fixture
:
@pytest.fixture(scope='package') def smtp_connection_package(): return smtplib.SMTP("smtp.163.com", 25, timeout=5)
接着,在src/chapter-4/package_expr/test_module1.py
中添加以下测试用例:
def test_ehlo_in_module1(smtp_connection_package): response, _ = smtp_connection_package.ehlo() assert response == 250 assert 0 # 为了展现,强制置为失败 def test_noop_in_module1(smtp_connection_package): response, _ = smtp_connection_package.noop() assert response == 250 assert 0 # 为了展现,强制置为失败
一样,在src/chapter-4/package_expr/test_module2.py
中添加以下测试用例:
def test_ehlo_in_module2(smtp_connection_package): response, _ = smtp_connection_package.ehlo() assert response == 250 assert 0 # 为了展现,强制置为失败
最后,执行src/chapter-4/package_expr
下全部的测试用例:
$ pipenv run pytest -q src/chapter-4/package_expr/ FFF [100%] =============================== FAILURES ================================ _________________________ test_ehlo_in_module1 __________________________ smtp_connection_package = <smtplib.SMTP object at 0x1028fec50> def test_ehlo_in_module1(smtp_connection_package): response, _ = smtp_connection_package.ehlo() assert response == 250 > assert 0 # 为了展现,强制置为失败 E assert 0 src/chapter-4/package_expr/test_module1.py:26: AssertionError _________________________ test_noop_in_module1 __________________________ smtp_connection_package = <smtplib.SMTP object at 0x1028fec50> def test_noop_in_module1(smtp_connection_package): response, _ = smtp_connection_package.noop() assert response == 250 > assert 0 E assert 0 src/chapter-4/package_expr/test_module1.py:32: AssertionError _________________________ test_ehlo_in_module2 __________________________ smtp_connection_package = <smtplib.SMTP object at 0x1028fec50> def test_ehlo_in_module2(smtp_connection_package): response, _ = smtp_connection_package.ehlo() assert response == 250 > assert 0 # 为了展现,强制置为失败 E assert 0 src/chapter-4/package_expr/test_module2.py:26: AssertionError 3 failed in 0.45s
能够看到:
fixture
实例,即<smtplib.SMTP object at 0x1028fec50>
;注意:
chapter-4/package_expr
能够不包含__init__.py
文件,由于pytest
发现测试用例的规则没有强制这一点;一样,package_expr/
的命名也不须要符合test_*
或者*_test
的规则;这个功能标记为实验性的,若是在其实际应用中发现严重的
bug
,那么这个功能极可能被移除;
fixture
的实例化顺序多个fixture
的实例化顺序,遵循如下原则:
session
)先于低级别的做用域的(例如:class
或者function
)实例化;fixture
之间的相互调用关系;autouse
的fixture
,先于其同级别的其它fixture
实例化;咱们来看一个具体的例子:
# src/chapter-4/test_order.py import pytest order = [] @pytest.fixture(scope="session") def s1(): order.append("s1") @pytest.fixture(scope="module") def m1(): order.append("m1") @pytest.fixture def f1(f3): order.append("f1") @pytest.fixture def f3(): order.append("f3") @pytest.fixture(autouse=True) def a1(): order.append("a1") @pytest.fixture def f2(): order.append("f2") def test_order(f1, m1, f2, s1): assert order == ["s1", "m1", "a1", "f3", "f1", "f2"]
s1
拥有最高级的做用域(session
),即便在测试用例test_order
中最后被声明,它也是第一个被实例化的(参照第一条原则)m1
拥有仅次于session
级别的做用域(module
),因此它是第二个被实例化的(参照第一条原则)f1 f2 f3 a1
同属于function
级别的做用域:
test_order(f1, m1, f2, s1)
形参的声明顺序中,能够看出,f1
比f2
先实例化(参照第二条原则)f1
的定义中又显式的调用了f3
,因此f3
比f1
先实例化(参照第二条原则)a1
的定义中使能了autouse
标记,因此它会在同级别的fixture
以前实例化,这里也就是在f3 f1 f2
以前实例化(参照第三条原则)因此这个例子fixture
实例化的顺序为:s1 m1 a1 f3 f1 f2
注意:
除了
autouse
的fixture
,须要测试用例显示声明(形参),不声明的不会被实例化;多个相同做用域的
autouse fixture
,其实例化顺序遵循fixture
函数名的排序;
fixture
的清理操做咱们指望在fixture
退出做用域以前,执行某些清理性操做(例如,关闭服务器的链接等);
咱们有如下几种形式,实现这个功能:
yield
代替return
将fixture
函数中的return
关键字替换成yield
,则yield
以后的代码,就是咱们要的清理操做;
咱们来声明一个包含清理操做的smtp_connection
:
# src/chapter-4/conftest.py @pytest.fixture() def smtp_connection_yield(): smtp_connection = smtplib.SMTP("smtp.163.com", 25, timeout=5) yield smtp_connection print("关闭SMTP链接") smtp_connection.close()
再添加一个使用它的测试用例:
# src/chapter-4/test_smtpsimple.py def test_ehlo_yield(smtp_connection_yield): response, _ = smtp_connection_yield.ehlo() assert response == 250 assert 0 # 为了展现,强制置为失败
如今,咱们来执行它:
λ pipenv run pytest -q -s --tb=no src/chapter-4/test_smtpsimple.py::test_ehlo_yield F关闭SMTP链接 1 failed in 0.18s
咱们能够看到在test_ehlo_yield
执行完后,又执行了yield
后面的代码;
with
写法对于支持with
写法的对象,咱们也能够隐式的执行它的清理操做;
例如,上面的smtp_connection_yield
也能够这样写:
@pytest.fixture() def smtp_connection_yield(): with smtplib.SMTP("smtp.163.com", 25, timeout=5) as smtp_connection: yield smtp_connection
addfinalizer
方法fixture
函数可以接收一个request
的参数,表示测试请求的上下文;咱们可使用request.addfinalizer
方法为fixture
添加清理函数;
例如,上面的smtp_connection_yield
也能够这样写:
@pytest.fixture() def smtp_connection_fin(request): smtp_connection = smtplib.SMTP("smtp.163.com", 25, timeout=5) def fin(): smtp_connection.close() request.addfinalizer(fin) return smtp_connection
注意:
在
yield
以前或者addfinalizer
注册以前代码发生错误退出的,都不会再执行后续的清理操做
fixture
能够访问测试请求的上下文fixture
函数能够接收一个request
的参数,表示测试用例、类、模块,甚至测试会话的上下文环境;
咱们能够扩展上面的smtp_connection_yield
,让其根据不一样的测试模块使用不一样的服务器:
# src/chapter-4/conftest.py @pytest.fixture(scope='module') def smtp_connection_request(request): server, port = getattr(request.module, 'smtp_server', ("smtp.163.com", 25)) with smtplib.SMTP(server, port, timeout=5) as smtp_connection: yield smtp_connection print("断开 %s:%d" % (server, port))
在测试模块中指定smtp_server
:
# src/chapter-4/test_request.py smtp_server = ("mail.python.org", 587) def test_163(smtp_connection_request): response, _ = smtp_connection_request.ehlo() assert response == 250
咱们来看看效果:
λ pipenv run pytest -q -s src/chapter-4/test_request.py .断开 mail.python.org:587 1 passed in 4.03s
fixture
返回工厂函数若是你须要在一个测试用例中,屡次使用同一个fixture
实例,相对于直接返回数据,更好的方法是返回一个产生数据的工厂函数;
而且,对于工厂函数产生的数据,也能够在fixture
中对其管理:
@pytest.fixture 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() def test_customer_records(make_customer_record): customer_1 = make_customer_record("Lisa") customer_2 = make_customer_record("Mike") customer_3 = make_customer_record("Meredith")
fixture
的参数化若是你须要在一系列的测试用例的执行中,每轮执行都使用同一个fixture
,可是有不一样的依赖场景,那么能够考虑对fixture
进行参数化;这种方式适用于对多场景的功能模块进行详尽的测试;
在以前的章节fixture能够访问测试请求的上下文中,咱们在测试模块中指定不一样smtp_server
,获得不一样的smtp_connection
实例;
如今,咱们能够经过指定params
关键字参数建立两个fixture
实例,每一个实例供一轮测试使用,全部的测试用例执行两遍;在fixture
的声明函数中,可使用request.param
获取当前使用的入参;
# src/chapter-4/test_request.py @pytest.fixture(scope='module', params=['smtp.163.com', "mail.python.org"]) def smtp_connection_params(request): server = request.param with smtplib.SMTP(server, 587, timeout=5) as smtp_connection: yield smtp_connection
在测试用例中使用这个fixture
:
# src/chapter-4/test_params.py def test_parames(smtp_connection_params): response, _ = smtp_connection_params.ehlo() assert response == 250
执行:
$ pipenv run pytest -q -s src/chapter-4/test_params.py .断开 smtp.163.com:25 .断开 smtp.126.com:25 2 passed in 0.26s
能够看到:
SMTP
服务器,执行了两次;在参数化的fixture
中,pytest
为每一个fixture
实例自动指定一个测试ID
,例如:上述示例中的test_parames[smtp.163.com]
和test_parames[smtp.126.com]
;
使用-k
选项执行一个指定的用例:
$ pipenv run pytest -q -s -k 163 src/chapter-4/test_params.py .断开 smtp.163.com:25 1 passed, 1 deselected in 0.16s
使用--collect-only
能够显示这些测试ID
,而不执行用例:
$ pipenv run pytest -q -s --collect-only src/chapter-4/test_params.py src/chapter-4/test_params.py::test_parames[smtp.163.com] src/chapter-4/test_params.py::test_parames[smtp.126.com] no tests ran in 0.01s
同时,也可使用ids
关键字参数,自定义测试ID
:
# src/chapter-4/test_ids.py @pytest.fixture(params=[0, 1], ids=['spam', 'ham']) def a(request): return request.param def test_a(a): pass
执行--collect-only
:
$ pipenv run pytest -q -s --collect-only src/chapter-4/test_ids.py::test_a src/chapter-4/test_ids.py::test_a[spam] src/chapter-4/test_ids.py::test_a[ham] no tests ran in 0.01s
咱们看到,测试ID
为咱们指定的值;
数字、字符串、布尔值和None
在测试ID
中使用的是它们的字符串表示形式:
# src/chapter-4/test_ids.py def idfn(fixture_value): if fixture_value == 0: return "eggs" elif fixture_value == 1: return False elif fixture_value == 2: return None else: return fixture_value @pytest.fixture(params=[0, 1, 2, 3], ids=idfn) def b(request): return request.param def test_b(b): pass
执行--collect-only
:
$ pipenv run pytest -q -s --collect-only src/chapter-4/test_ids.py::test_b src/chapter-4/test_ids.py::test_b[eggs] src/chapter-4/test_ids.py::test_b[False] src/chapter-4/test_ids.py::test_b[2] src/chapter-4/test_ids.py::test_b[3] no tests ran in 0.01s
能够看到:
ids
能够接收一个函数,用于生成测试ID
;ID
指定为None
时,使用的是params
原先对应的值;注意:
当测试
params
中包含元组、字典或者对象时,测试ID
使用的是fixture
函数名+param
的下标:
# src/chapter-4/test_ids.py class C: pass @pytest.fixture(params=[(1, 2), {'d': 1}, C()]) def c(request): return request.param def test_c(c): pass执行
--collect-only
:$ pipenv run pytest -q -s --collect-only src/chapter-4/test_ids.py::test_c src/chapter-4/test_ids.py::test_c[c0] src/chapter-4/test_ids.py::test_c[c1] src/chapter-4/test_ids.py::test_c[c2] no tests ran in 0.01s能够看到,测试
ID
为fixture
的函数名(c
)加上对应param
的下标(从0
开始);若是你不想这样,可使用
str()
方法或者复写__str__()
方法;
fixture
中标记用例在fixture
的params
参数中,可使用pytest.param
标记这一轮的全部用例,其用法和在pytest.mark.parametrize
中的用法同样;
# src/chapter-4/test_fixture_marks.py import pytest @pytest.fixture(params=[('3+5', 8), pytest.param(('6*9', 42), marks=pytest.mark.xfail, id='failed')]) def data_set(request): return request.param def test_data(data_set): assert eval(data_set[0]) == data_set[1]
咱们使用pytest.param(('6*9', 42), marks=pytest.mark.xfail, id='failed')
的形式指定一个request.param
入参,其中marks
表示当用例使用这个入参时,为这个用例打上xfail
标记;而且,咱们还使用id
为此时的用例指定了一个测试ID
;
$ pipenv run pytest -v src/chapter-4/test_fixture_marks.py::test_data ============================ test session starts ============================ platform darwin -- Python 3.7.3, pytest-5.1.3, py-1.8.0, pluggy-0.13.0 -- /Users/yaomeng/.local/share/virtualenvs/pytest-chinese-doc-EK3zIUmM/bin/python3.7 cachedir: .pytest_cache rootdir: /Users/yaomeng/Private/Projects/pytest-chinese-doc collected 2 items src/chapter-4/test_fixture_marks.py::test_data[data_set0] PASSED [ 50%] src/chapter-4/test_fixture_marks.py::test_data[failed] XFAIL [100%] ======================= 1 passed, 1 xfailed in 0.08s ========================
能够看到:
XFAIL
,而不是FAILED
;ID
是咱们指定的failed
,而不是data_set1
;咱们也可使用pytest.mark.parametrize
实现相同的效果:
# src/chapter-4/test_fixture_marks.py @pytest.mark.parametrize( 'test_input, expected', [('3+5', 8), pytest.param('6*9', 42, marks=pytest.mark.xfail, id='failed')]) def test_data2(test_input, expected): assert eval(test_input) == expected
执行:
pipenv run pytest -v src/chapter-4/test_fixture_marks.py::test_data2 ============================ test session starts ============================ platform darwin -- Python 3.7.3, pytest-5.1.3, py-1.8.0, pluggy-0.13.0 -- /Users/yaomeng/.local/share/virtualenvs/pytest-chinese-doc-EK3zIUmM/bin/python3.7 cachedir: .pytest_cache rootdir: /Users/yaomeng/Private/Projects/pytest-chinese-doc collected 2 items src/chapter-4/test_fixture_marks.py::test_data2[3+5-8] PASSED [ 50%] src/chapter-4/test_fixture_marks.py::test_data2[failed] XFAIL [100%] ======================= 1 passed, 1 xfailed in 0.07s ========================
fixture
使用其它的fixture
你不只仅能够在测试用例上使用fixture
,还能够在fixture
的声明函数中使用其它的fixture
;这有助于模块化的设计你的fixture
,能够在多个项目中重复使用框架级别的fixture
;
一个简单的例子,咱们能够扩展以前src/chapter-4/test_params.py
的例子,实例一个app
对象:
# src/chapter-4/test_appsetup.py import pytest class App: def __init__(self, smtp_connection): self.smtp_connection = smtp_connection @pytest.fixture(scope='module') def app(smtp_connection_params): return App(smtp_connection_params) def test_smtp_connection_exists(app): assert app.smtp_connection
咱们建立一个fixture app
并调用以前在conftest.py
中定义的smtp_connection_params
,返回一个App
的实例;
执行:
$ pipenv run pytest -v src/chapter-4/test_appsetup.py ============================ test session starts ============================ platform darwin -- Python 3.7.3, pytest-5.1.3, py-1.8.0, pluggy-0.13.0 -- /Users/yaomeng/.local/share/virtualenvs/pytest-chinese-doc-EK3zIUmM/bin/python3.7 cachedir: .pytest_cache rootdir: /Users/yaomeng/Private/Projects/pytest-chinese-doc collected 2 items src/chapter-4/test_appsetup.py::test_smtp_connection_exists[smtp.163.com] PASSED [ 50%] src/chapter-4/test_appsetup.py::test_smtp_connection_exists[smtp.126.com] PASSED [100%] ============================= 2 passed in 1.25s =============================
由于app
使用了参数化的smtp_connection_params
,因此测试用例test_smtp_connection_exists
会使用不一样的App
实例执行两次,而且,app
并不须要关心smtp_connection_params
的实现细节;
app
的做用域是模块级别的,它又调用了smtp_connection_params
,也是模块级别的,若是smtp_connection_params
是会话级别的做用域,这个例子仍是同样能够正常工做的;这是由于低级别的做用域能够调用高级别的做用域,可是高级别的做用域调用低级别的做用域会返回一个ScopeMismatch
的异常;
fixture
实例在测试期间,pytest
只激活最少个数的fixture
实例;若是你拥有一个参数化的fixture
,全部使用它的用例会在建立的第一个fixture
实例并销毁后,才会去使用第二个实例;
下面这个例子,使用了两个参数化的fixture
,其中一个是模块级别的做用域,另外一个是用例级别的做用域,而且使用print
方法打印出它们的setup/teardown
流程:
# src/chapter-4/test_minfixture.py import pytest @pytest.fixture(scope="module", params=["mod1", "mod2"]) def modarg(request): param = request.param print(" SETUP modarg", param) yield param print(" TEARDOWN modarg", param) @pytest.fixture(scope="function", params=[1, 2]) def otherarg(request): param = request.param print(" SETUP otherarg", param) yield param print(" TEARDOWN otherarg", param) def test_0(otherarg): print(" RUN test0 with otherarg", otherarg) def test_1(modarg): print(" RUN test1 with modarg", modarg) def test_2(otherarg, modarg): print(" RUN test2 with otherarg {} and modarg {}".format(otherarg, modarg))
执行:
$ pipenv run pytest -q -s src/chapter-4/test_minfixture.py SETUP otherarg 1 RUN test0 with otherarg 1 . TEARDOWN otherarg 1 SETUP otherarg 2 RUN test0 with otherarg 2 . TEARDOWN otherarg 2 SETUP modarg mod1 RUN test1 with modarg mod1 . SETUP otherarg 1 RUN test2 with otherarg 1 and modarg mod1 . TEARDOWN otherarg 1 SETUP otherarg 2 RUN test2 with otherarg 2 and modarg mod1 . TEARDOWN otherarg 2 TEARDOWN modarg mod1 SETUP modarg mod2 RUN test1 with modarg mod2 . SETUP otherarg 1 RUN test2 with otherarg 1 and modarg mod2 . TEARDOWN otherarg 1 SETUP otherarg 2 RUN test2 with otherarg 2 and modarg mod2 . TEARDOWN otherarg 2 TEARDOWN modarg mod2 8 passed in 0.02s
能够看出:
mod1
的TEARDOWN
操做完成后,才开始mod2
的SETUP
操做;test_0
独立完成测试;test_1
和test_2
都使用到了模块级别的modarg
,同时test_2
也使用到了用例级别的otherarg
。它们执行的顺序是,test_1
先使用mod1
,接着test_2
使用mod1
和otherarg 1/otherarg 2
,而后test_1
使用mod2
,最后test_2
使用mod2
和otherarg 1/otherarg 2
;也就是说test_1
和test_2
共用相同的modarg
实例,最少化的保留fixture
的实例个数;fixture
实例有时,咱们并不须要在测试用例中直接使用fixture
实例;例如,咱们须要一个空的目录做为当前用例的工做目录,可是咱们并不关心如何建立这个空目录;这里咱们可使用标准的tempfile模块来实现这个功能;
# src/chapter-4/conftest.py import pytest import tempfile import os @pytest.fixture() def cleandir(): newpath = tempfile.mkdtemp() os.chdir(newpath)
在测试中使用usefixtures
标记声明使用它:
# src/chapter-4/test_setenv.py import os import pytest @pytest.mark.usefixtures("cleandir") class TestDirectoryInit: def test_cwd_starts_empty(self): assert os.listdir(os.getcwd()) == [] with open("myfile", "w") as f: f.write("hello") def test_cwd_again_starts_empty(self): assert os.listdir(os.getcwd()) == []
得益于usefixtures
标记,测试类TestDirectoryInit
中全部的测试用例均可以使用cleandir
,这和在每一个测试用例中指定cleandir
参数是同样的;
执行:
$ pipenv run pytest -q -s src/chapter-4/test_setenv.py .. 2 passed in 0.02s
你可使用以下方式指定多个fixture
:
@pytest.mark.usefixtures("cleandir", "anotherfixture") def test(): ...
你也可使用以下方式为测试模块指定fixture
:
pytestmark = pytest.mark.usefixtures("cleandir")
注意:参数的名字必须是
pytestmark
;
你也可使用以下方式为整个项目指定fixture
:
# src/chapter-4/pytest.ini [pytest] usefixtures = cleandir
注意:
usefixtures
标记不适用于fixture
声明函数;例如:@pytest.mark.usefixtures("my_other_fixture") @pytest.fixture def my_fixture_that_sadly_wont_use_my_other_fixture(): ...这并不会返回任何的错误或告警,具体讨论能够参考#3664
fixture
有时候,你想在测试用例中自动使用fixture
,而不是做为参数使用或者usefixtures
标记;设想,咱们有一个数据库相关的fixture
,包含begin/rollback/commit
的体系结构,如今咱们但愿经过begin/rollback
包裹每一个测试用例;
下面,经过列表实现一个虚拟的例子:
# src/chapter-4/test_db_transact.py import pytest class DB: def __init__(self): self.intransaction = [] def begin(self, name): self.intransaction.append(name) def rollback(self): self.intransaction.pop() @pytest.fixture(scope="module") def db(): return DB() class TestClass: @pytest.fixture(autouse=True) def transact(self, request, db): db.begin(request.function.__name__) yield db.rollback() def test_method1(self, db): assert db.intransaction == ["test_method1"] def test_method2(self, db): assert db.intransaction == ["test_method2"]
类级别做用域的transact
函数中声明了autouse=True
,因此TestClass
中的全部用例,能够自动调用transact
而不用显式的声明或标记;
执行:
$ pipenv run pytest -q -s src/chapter-4/test_db_transact.py .. 2 passed in 0.01s
autouse=True
的fixture
在其它级别做用域中的工做流程:
autouse fixture
遵循scope
关键字的定义:若是其含有scope='session'
,则无论它在哪里定义的,都将只执行一次;scope='class'
表示每一个测试类执行一次;autouse fixture
,那么这个测试模块全部的用例自动使用它;conftest.py
中定义autouse fixture
,那么它的相同文件夹和子文件夹中的全部测试模块中的用例都将自动使用它;autouse fixture
,那么全部安装这个插件的项目中的全部用例都将自动使用它;上述的示例中,咱们指望只有TestClass
的用例自动调用fixture transact
,这样咱们就不但愿transact
一直处于激活的状态,因此更标准的作法是,将transact
声明在conftest.py
中,而不是使用autouse=True
:
@pytest.fixture def transact(request, db): db.begin() yield db.rollback()
而且,在TestClass
上声明:
@pytest.mark.usefixtures("transact") class TestClass: def test_method1(self): ...
其它类或者用例也想使用的话,一样须要显式的声明usefixtures
;
fixture
在大型的测试中,你可能须要在本地覆盖项目级别的fixture
,以增长可读性和便于维护;
conftest.py
)层级覆写fixture
假设咱们有以下的测试项目:
tests/ __init__.py conftest.py # content of tests/conftest.py import pytest @pytest.fixture def username(): return 'username' test_something.py # content of tests/test_something.py def test_username(username): assert username == 'username' subfolder/ __init__.py conftest.py # content of tests/subfolder/conftest.py import pytest @pytest.fixture def username(username): return 'overridden-' + username test_something.py # content of tests/subfolder/test_something.py def test_username(username): assert username == 'overridden-username'
能够看到:
conftest.py
中的fixture
覆盖了上层文件夹中同名的fixture
;conftest.py
中的fixture
能够轻松的访问上层文件夹中同名的fixture
;fixture
假设咱们有以下的测试项目:
tests/ __init__.py conftest.py # content of tests/conftest.py import pytest @pytest.fixture def username(): return 'username' test_something.py # content of tests/test_something.py import pytest @pytest.fixture def username(username): return 'overridden-' + username def test_username(username): assert username == 'overridden-username' test_something_else.py # content of tests/test_something_else.py import pytest @pytest.fixture def username(username): return 'overridden-else-' + username def test_username(username): assert username == 'overridden-else-username'
能够看到:
fixture
覆盖了conftest.py
中同名的fixture
;fixture
能够轻松的访问conftest.py
中同名的fixture
;fixture
假设咱们有以下的测试项目:
tests/ __init__.py conftest.py # content of tests/conftest.py import pytest @pytest.fixture def username(): return 'username' @pytest.fixture def other_username(username): return 'other-' + username test_something.py # content of tests/test_something.py import pytest @pytest.mark.parametrize('username', ['directly-overridden-username']) def test_username(username): assert username == 'directly-overridden-username' @pytest.mark.parametrize('username', ['directly-overridden-username-other']) def test_username_other(other_username): assert other_username == 'other-directly-overridden-username-other'
能够看到:
fixture
的值被用例的参数所覆盖;test_username_other
没有使用username
,可是other_username
使用到了username
,因此也一样受到了影响;fixture
覆写非参数化的fixture
,反之亦然tests/ __init__.py conftest.py # content of tests/conftest.py import pytest @pytest.fixture(params=['one', 'two', 'three']) def parametrized_username(request): return request.param @pytest.fixture def non_parametrized_username(request): return 'username' test_something.py # content of tests/test_something.py import pytest @pytest.fixture def parametrized_username(): return 'overridden-username' @pytest.fixture(params=['one', 'two', 'three']) def non_parametrized_username(request): return request.param def test_username(parametrized_username): assert parametrized_username == 'overridden-username' def test_parametrized_username(non_parametrized_username): assert non_parametrized_username in ['one', 'two', 'three'] test_something_else.py # content of tests/test_something_else.py def test_username(parametrized_username): assert parametrized_username in ['one', 'two', 'three'] def test_username(non_parametrized_username): assert non_parametrized_username == 'username'
能够看出:
fixture
和非参数化的fixture
一样能够相互覆盖;