Python pytest fixture

By | 7月 24, 2023

用@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的优点在于:

  1. 可以通过addfinalizer添加多个函数,后添加的先执行。
  2. 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个值可选:

  1. function (default):每个测试函数都会调用,不共享
  2. class:每个class调用一次
  3. module:每个module (.py) 调用一次
  4. package:每个package调用一次
  5. 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%]