pytest fixtures
1、fixture
测试用例可以接收fixture
的名字作为入参,其实参是对应的fixture
函数的返回值。通过@pytest.fixture
装饰器可以注册一个fixture
;
- fixture作为函数参数
import pytest @pytest.fixture def login(): print('输入用户名及密码,登录系统') def test_shopping_buy(login): print('提交商品信息')
在购买商品时,需要先登录系统,于是将login作为函数参数,传递至商品购买测试函数。结果:
test03.py .搜索商品信息 输入用户名及密码,登录系统 .提交商品信息 [100%] ============================== 2 passed in 0.03s ==============================
2、conftest
如果在实施测试期间您意识到要使用多个测试文件中的夹具功能,则可以将其移至一个conftest.py文件中。您不需要导入要在测试中使用的fixture,它会被pytest自动发现。fixture功能的发现顺序,始于测试类,接着是测试模块,然后是 conftest.py文件,最后是内置插件和第三方插件。
#内容在conftest.py文件 import pytest @pytest.fixture def login(): print('输入用户名及密码,登录系统')
#内容在test03.py文件 import pytest def test_shopping_sou(): print('搜索商品信息') def test_shopping_buy(login): print('提交商品信息')
结果,在执行test03.py文件时,登录信息一样被打印出来,并且购买商品函数自动调用了conftest.py文件中的login函数。
test03.py .搜索商品信息 输入用户名及密码,登录系统 .提交商品信息 [100%] ============================== 2 passed in 0.03s ==============================
记住:fixture功能的发现顺序,始于测试类,接着是测试模块,然后是 conftest.py文件,最后是内置插件和第三方插件。
-
类,模块或会话中的测试之间共享夹具实例
在执行测试时,有时候只希望某些方法只在类中调用一次,或者在一个模块中调用一次,而不是让期在每个方法是上都调用。比如,我们只想在这个模块中调用登录函数,我们可以scope="module"在@pytest.fixture调用中添加一个参数, 以使经过修饰的login夹具函数在每个测试模块中仅被调用一次(默认值为每个测试函数一次)。
scope参数有:function,class,module,package或session。
#内容在conftest.py文件 import pytest @pytest.fixture(scope="module") def login(): print('输入用户名及密码,登录系统')
#内容在test03.py文件 import pytest def test_shopping_sou(login): print('搜索商品信息') def test_shopping_select(login): print('选择商品信息') def test_shopping_submit(login): print('提交商品信息') def test_shopping_buy(login): print('购买商品信息')
结果,在执行测试时,用户只登录了一次,并完成了购买
test03.py 输入用户名及密码,登录系统 .搜索商品信息 .选择商品信息 .提交商品信息 .购买商品信息 [100%] ============================== 4 passed in 0.04s ==============================
如果修改为默认级别function,那么每个测试函数将会执行登录操作
#内容在conftest.py文件 import pytest #为空时默认为function级别,其实和@pytest.fixture(scope="function")相同 @pytest.fixture() def login(): print('输入用户名及密码,登录系统')
结果如下所示
test03.py 输入用户名及密码,登录系统 .搜索商品信息 输入用户名及密码,登录系统 .选择商品信息 输入用户名及密码,登录系统 .提交商品信息 输入用户名及密码,登录系统 .购买商品信息 [100%] ============================== 4 passed in 0.04s ==============================
3、fixture的实例化顺序
多个fixture
的实例化顺序,遵循以下原则:
- 高级别作用域的(例如:
session
)先于低级别的作用域的(例如:class
或者function
)实例化; - 相同级别作用域的,其实例化顺序遵循它们在测试用例中被声明的顺序(也就是形参的顺序),或者
fixture
之间的相互调用关系; - 使能
autouse
的fixture
,先于其同级别的其它fixture
实例化;
注意:
- 除了
autouse
的fixture
,需要测试用例显示声明(形参),不声明的不会被实例化;- 多个相同作用域的
autouse fixture
,其实例化顺序遵循fixture
函数名的排序;
4、fixture的初始化和资源释放
一般在执行测试用例之前需要执行一些初始化操作,比如打开浏览器或者准备测试数据,完成测试后又需要释放资源,比如关闭浏览器或者销毁数据,在pytest中可以使用yield进行操作,相当于setup和teardown。
#内容在test04.py文件 import pytest def test_shopping_sou(open_browser,login): print('搜索商品信息') def test_shopping_buy(login): print('购买商品信息')
#内容在conftest.py文件 import pytest #为空时默认为function级别,其实和@pytest.fixture(scope="function")相同 @pytest.fixture(scope="module") def login(): print('输入用户名及密码,登录系统') @pytest.fixture(scope="module") def open_browser(): print("打开浏览器") yield print("关闭浏览器")
结果:执行一次打开浏览器,测试完毕后自动关闭浏览器,释放资源。
test04.py 打开浏览器 输入用户名及密码,登录系统 .搜索商品信息 .购买商品信息 关闭浏览器 [100%] ============================== 2 passed in 0.04s ==============================
5、fixture的参数化
如果你需要在一系列的测试用例的执行中,每轮执行都使用同一个fixture
,但是有不同的依赖场景,那么可以考虑对fixture
进行参数化;这种方式适用于对多场景的功能模块进行详尽的测试;
#内容在test.py文件 import pytest @pytest.fixture(scope="module") def login(): print('登录系统') yield print("退出系统") def test_shopping_sou(open_browser,login): print('搜索商品信息:{0}'.format(open_browser))
#内容在conftest.py文件 import pytest @pytest.fixture(scope="module",params=['苹果','橘子','香蕉']) def open_browser(request): return request.param
在conftest.py文件中对
fixture
进行参数化,结果:test.py 登录系统 .搜索商品信息:苹果 .搜索商品信息:橘子 .搜索商品信息:香蕉 退出系统 [100%] ============================== 3 passed in 0.06s ==============================
6、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 ========================
7、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
的实例个数;
8、fixture的自动应用
有时,在测试用例执行中希望自动调用fixture
,而无需显式声明函数参数或usefixtures装饰器。作为一个实际的例子,假设我们有一个具有开始/回滚/提交架构的数据库设备,并且我们希望通过事务和回滚来自动包围每种测试方法。
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
而不用显式的声明或标记.。如果运行它,我们将通过两个测试:
$ pytest -q .. [100%] 2 passed in 0.12s
autouse=True
的fixture
在其它级别作用域中的工作流程:
autouse fixture
遵循scope
关键字的定义:如果其含有scope='session'
,则不管它在哪里定义的,都将只执行一次;scope='class'
表示每个测试类执行一次;- 如果在测试模块中定义
autouse fixture
,那么这个测试模块所有的用例自动使用它;- 如果在
conftest.py
中定义autouse fixture
,那么它的相同文件夹和子文件夹中的所有测试模块中的用例都将自动使用它;- 如果在插件中定义
autouse fixture
,那么所有安装这个插件的项目中的所有用例都将自动使用它;
9、fixture返回工厂函数
如果你需要在一个测试用例中,多次使用同一个fixture
实例,相对于直接返回数据,更好的方法是返回一个产生数据的工厂函数;并且,对于工厂函数产生的数据,也可以在fixture
中对其管理:
import pytest @pytest.fixture def make_customer_record(): def _make_customer_record(name): return { "name": name, "orders": [] } return _make_customer_record 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") print(customer_3,customer_2,customer_1)
其实,这个和python的闭包函数,装饰器是一样的用法。
参考:https://www.cnblogs.com/superhin/p/11733499.html