用@pytest.fixture装饰的函数就是fixture,该函数名可以当做测试方法的参数。执行测试方法时,pytest搜索与参数同名的fixture,并将其返回值注入给测试方法,然后执行测试方法。
换句话说,pytest测试方法的参数就是fixture函数的名字,如果不存在该fixture,运行就会报错。
下面是fixture方法签名,name参数用于给fixture起个别名,没有name,fixture名字是是所装饰的函数名字。其它参数后面用例子介绍。
fixture(scope="function",params=None,autouse=False,ids=None,name=None)
实现setUp()功能
setUp()用于在执行unit test前做些初始化工作,利用fixture注入,可以给测试方法注入准备好的数据。
import pytest @pytest.fixture def setup_numbs(): return [1, 2, 3] def test_sum(setup_numbs): assert sum(setup_numbs) == 6
实现setUp()和tearDown()功能
yield
setupUp()用于初始化操作,tearDown()用于清理工作,例如删除临时文件、关闭数据库等。
对于fixture函数,它既可以初始化数据,又可以做清理工作。它以yield关键字为界限,yield之上的代码会在注入给测试方法时调用,yeild之下的代码会在测试方法执行后调用。yield关键字相当于return,用于返回一个值注入给测试方法。
import pytest import tempfile @pytest.fixture def temp_session(): session_dir = tempfile.TemporaryDirectory() # init session yield session_dir session_dir.close() def test_load_session(temp_session): print(temp_session) # <TemporaryDirectory '...\\Temp\\tmp72japr_n'>
addfinalizer注册清理函数
fixture函数能够接收一个request参数,表示测试请求的上下文。可以使用request.addfinalizer方法为fixture添加清理销毁函数,实现tearDown()的功能。
import pytest import tempfile @pytest.fixture def temp_session(request): session_dir = tempfile.TemporaryDirectory() # Init session def clean_data(): session_dir.cleanup() request.addfinalizer(clean_data) return session_dir def test_load_session(temp_session): print(temp_session) # <TemporaryDirectory '...\\Temp\\tmp8um7dr80'>
句法上yield更简洁,但是addfinalizer的优点在于:
- 可以通过addfinalizer添加多个函数,后添加的先执行。
- yield前的语句如果有异常,yield后的代码将不会执行。但是addfinalizer只要注册了,一定会执行。
import pytest import tempfile @pytest.fixture def temp_session(request): session_dir = tempfile.TemporaryDirectory() def clean_data():... request.addfinalizer(clean_data) def close_db():... request.addfinalizer(close_db) return session_dir
共享fixture
scope
fixture(scope=”function”,params=None,autouse=False,ids=None,name=None)
fixture装饰器有个可选参数scope,控制fixture在上下文中是否共享,共享的fixture在指定上下文中只执行一次,返回结果会被缓存。scope有5个值可选:
- function (default):每个测试函数都会调用,不共享
- class:每个class调用一次
- module:每个module (.py) 调用一次
- package:每个package调用一次
- session:项目unit test执行期间调用一次
function scope
import pytest @pytest.fixture(scope='function') def setup(): print('\nExecute before test') yield print('\nExecute after test') def test_1(setup):... def test_2(setup):... ------------------------------------------ hello2_test.py::test_1 Execute before test PASSED [ 50%] Execute after test hello2_test.py::test_2 Execute before test PASSED [100%] Execute after test
class scope
import pytest @pytest.fixture(scope='class') def setup(): print('\nExecute before class') yield print('\nExecute after class') class TestSession: def test_1(self, setup):... def test_2(self, setup):... ---------------------------------------------------- fixture_test.py::TestSession::test_1 Execute before class PASSED [ 50%] fixture_test.py::TestSession::test_2 PASSED [100%] Execute after class
module, package, session类似,就不举例了。
conftest.py
可以在任何package里定义conftest.py文件,文件名是固定的,pytest你自动识别该文件。定义在里面fixture,可以被当前package以及子package里的测试方法使用。相当于在一个集中的地方声明fixture,便于管理和共享。
在当面目录下创建contest.py,创建setup fixture:
import pytest @pytest.fixture def setup(): print('\nExecute before test method') yield print('\nExecute after test method')
在测试文件里使用使用setup fixture:
def test_1(setup):... # ------------------------------------------------- fixture_test.py::test_1 Execute before test method PASSED [100%] Execute after test method
通过params生成多实例fixture
fixture(scope=”function”,params=None,autouse=False,ids=None,name=None)
params是个数组,会对每个元素生成一个fixture,每个fixture都会注入给测试方法执行,ids是数据对应的ID(显示在测试方法后面)。
如果没有ids,当params中元素是元祖、字典或者对象时,测试ID是fixture函数名+元素index;否则ID是元素本身。
import pytest fruits = ['apple', 'banana', 'perl'] fruit_ids = ['001', '021', '132'] @pytest.fixture(params=fruits, ids=fruit_ids) def fruit_name_len(request): print('\nBefore method') yield request.param print('\nAfter method') def test_1(fruit_name_len): print(f'\nFruit name length: {fruit_name_len}') ---------------------------------------------------- fixture_test.py::test_1[001] Before method PASSED [ 33%] Fruit name length: 5 After method fixture_test.py::test_1[021] Before method PASSED [ 66%] Fruit name length: 6 After method fixture_test.py::test_1[132] Before method PASSED [100%] Fruit name length: 4 After method
@pytest.mark.parametrize – 简单迭代执行
上面的例子用params参数,对每个元素生成一个fixture,有点heavy。
有时我们就想对各个数据迭代执行一次测试方法,然后比较结果是否正确,可以用@pytest.mark.parametrize。最后一个参数是iterable的data,前面的参数是data要被解析成什么变量。如果元素是元祖或list类型,则可以分解为多个变量。
import pytest test_data = [("3+5", 8), ("2+4", 6), ("6*9", 42)] @pytest.mark.parametrize("test_input, expected", test_data) def test_eval(test_input, expected): assert eval(test_input) == expected
自动调用fixture
如果每次使用fixture都要通过传参的方式,则应改变原来测试方法的结构。如何不通过注入的方式让测试方法执行呢?有2种方式可选,第一种在fixture的参数中将autouse参数设置为True,这样便会自动应用所作用的范围。第二种使用@pytest.mark.usefixtures,在需要的测试方法上添加。
autouse=True
import pytest @pytest.fixture(autouse=True) def open(): print('\nOpen home page') def test_login():... def test_search():... def test_logout():... ----------------------------------------- fixture_auto_test.py::test_login Open home page PASSED [ 25%] fixture_auto_test.py::test_search Open home page PASSED [ 50%] fixture_auto_test.py::test_logout Open home page PASSED [ 75%] fixture_test.py::test_1 PASSED [100%]
注意:测试中,上例的open fixture只对本测试文件的测试方法有效;同目录的另外一个测试文件的测试方法没有应用到(看最后一行fixture_test.py),即使import了fixture所在的module也没用。
如果要让open fixture应用到所有测试方法,可以把它声明在conftest.py文件里。
@pytest.mark.usefixtures
下例中test_search会调用open, login;test_view啥也没调用。
import pytest @pytest.fixture def open(): print('\nOpen home page') @pytest.fixture() def login(): print('\nLogin APP') @pytest.mark.usefixtures('open', 'login') def test_search():... def test_view():... ------------------------------------------ fixture_auto_test.py::test_search Open home page Login APP PASSED [ 50%] fixture_auto_test.py::test_view PASSED [100%]