03-fixture 功能

3. fixture 功能

3.1 fixture 介绍

    fixture 是 pytest 独有功能,使用 @pytest.fixture 标识,在函数前面定义。在编写测试函数的时候,可以将此函数的名称作为传入参数,pytest会以依赖注入方式将该函数的返回值作为测试函数的传入参数。
    fixture 的管理可以从简单的单元测试扩展到复杂的功能测试,允许通过配置和组件选项参数化fixture,也可以实现跨功能、类、模块、会话级别的复用fixture。同时pytest也支持经典的xUnit风格的测试。

3.2 fixture 目标

    fixture 主要目的是提供一种可靠和可重复的手段去运行最基本的测试内容。例如部分用例在运行时,需要进行登录和退出,可以利用fixture,仅执行一次,否则每个测试用例都要执行一次,显得非常冗余。相对经典的setup/teardown这些固定名称,fixture有一个明确的名称,通过声明使其能够在函数、类、模块、会话级别中使用。
    fixture 是以一种模块化的方法实现,因为每一个fixture的名字都能触发一个fixture函数,而这个函数本身又能调用其他的fixture,即fixture嵌套功能。同时,fixture也实现了参数化功能,可根据配置和不同组件选择不同的参数。

3.3 fixture 基本依赖注入功能

    fixture 允许测试用例可以轻松接收和处理特定的需要初始化操作的应用对象,而不用过分关心导入、设置、清理的细节,这是一个非常典型的依赖注入的实践。在这个过程,fixture扮演注入者(injector)角色,测试用例扮演消费者的角色(client)。

    测试用例执行时,有些模块需要提前执行,然后再执行其他用例。之前都是使用setup的方式把需要提前执行的用例放在其中,这样所有测试用例都要先执行setup方法,而部分测试用例并不需要。而fixture的依赖注入功能可以灵活解决此类问题。

    来看一个在测试过程常见的登录应用场景,示例代码如下所示:

# @IDE:      PyCharm
# @Project:  PyCharmProjects
# @File:     test_fixture_01.py
# @Time      2025-03-11 22:43
# @Author:   Surpass Lee
# @E-mail:   surpassmebyme@gmail.com

import pytest

@pytest.fixture()
def login():
    print("\n01:用户登录")

def test_add_user(login):
    print("\n02:添加用户")

def test_del_user(login):
    print("\n03:删除用户")

if __name__ == '__main__':
    pytest.main(["-s","test_fixture_01.py"])

    运行结果如下所示:

============================= test session starts =============================
collecting ... collected 2 items

test_fixture_01.py::test_add_user 
01:用户登录
PASSED                                 [ 50%]
02:添加用户

test_fixture_01.py::test_del_user 
01:用户登录
PASSED                                 [100%]
03:删除用户


============================== 2 passed in 0.03s ==============================

@pytest.fixture()不设置参数scope时,则默认scope="function"

3.4 fixture 初始化设置

    初始化过程一般在进行数据初始化、连接初始化等场景。例如测试用例执行时,有的用例数据是可读取的,需要把数据读取后再执行测试用例,setup/teardown可以实现,但fixture可以灵活命名实现。示例代码如下所示:

# @IDE:      PyCharm
# @Project:  PyCharmProjects
# @File:     test_fixture_02.py
# @Time      2025-03-11 22:52
# @Author:   Surpass Lee
# @E-mail:   surpassmebyme@gmail.com

import pytest

@pytest.fixture()
def get_data():
    test_data={
        "username":"surpass",
        "passwd":"passwd"
    }
    return  test_data

def login(username:str,passwd:str)->str:
    login_result="登录失败"
    if username == "surpass" and passwd == "passwd":
        login_result="登录成功"
    return login_result

def test_login(get_data):
    username,passwd=get_data.get("username"),get_data.get("passwd")
    print(f"\n01:获取到的用户名和密码为:{username} - {passwd}")
    result=login(username,passwd)
    assert result.__contains__("成功"),"登录验证失败"

    如果测试数据是从外部获取,也可以使用fixture来实现同样的功能,以csv文件为例,代码如下所示:

  • csv文件内容如下所示:
username,passwd
surpass,surpass
kevin,kevin
ben,ben
diana,passwd
  • python代码如下所示:
# @IDE:      PyCharm
# @Project:  PyCharmProjects
# @File:     test_fixture_03.py
# @Time      2025-03-11 22:59
# @Author:   Surpass Lee
# @E-mail:   surpassmebyme@gmail.com

import csv
import pytest

@pytest.fixture()
def get_data():
    with open("data.csv",mode="r",encoding="utf-8") as fo:
        rows = csv.reader(fo,delimiter=",")
        next(rows)
        users=[]
        for item in rows:
            users.append(item)
    return users

def login(username:str,passwd:str)->str:
    login_result="登录失败"
    if username == passwd :
        login_result="登录成功"
    return login_result

def test_login(get_data):
    for item in get_data:
        username, passwd=item
        print(f"\n01:获取到的用户名和密码为:{username} - {passwd}")
        result = login(username, passwd)
        assert result.__contains__("成功"), "登录验证失败"

3.5 fixture 销毁配置

3.5.1 使用yield 代替 return

    通过前面的fixture已经可以将需要提前执行的步骤解决,但在有些场景下,还需要在测试用例执行完成,清理和恢复为初始化环境,范围为模块级别,这时应该如何处理呢?在pytest中,可以在同一模块中使用 yield 关键字。yield调用第一次返回结果,第二次执行后续的语句并再返回结果。示例代码如下所示:

# @IDE:      PyCharm
# @Project:  PyCharmProjects
# @File:     test_fixture_04.py
# @Time      2025-03-11 23:18
# @Author:   Surpass Lee
# @E-mail:   surpassmebyme@gmail.com

import pytest

@pytest.fixture()
def get_data():
    test_data = {
        "username": "surpass",
        "passwd": "passwd"
    }
    return test_data


@pytest.fixture(scope="function")
def login(get_data):
    print("\n01:执行登录")

    yield

    print("\n02:退出登录")


def test_login_01(login):
    print("\n:执行用例-01")

def test_login_02(login):
    print("\n:执行用例-02")

    运行结果如下所示:

collecting ... collected 2 items

test_fixture_04.py::test_login_01 
01:执行登录
PASSED                                 [ 50%]
:执行用例-01

02:退出登录

test_fixture_04.py::test_login_02 
01:执行登录
PASSED                                 [100%]
:执行用例-02

02:退出登录


============================== 2 passed in 0.02s ==============================

3.5.2 使用with

    对于支持with语法的对象,程序可以隐式执行其清理销毁操作。在使用with出现问题时,可以由Python进行处理和销毁。示例代码如下所示:

# @IDE:      PyCharm
# @Project:  PyCharmProjects
# @File:     test_fixture_05.py
# @Time      2025-03-11 23:26
# @Author:   Surpass Lee
# @E-mail:   surpassmebyme@gmail.com
import pymysql
import pytest


@pytest.fixture()
def mysql_connection():
    with pymysql.connect(host="127.0.0.1",
                        port=3306,
                        user="user",
                        password="passwd",
                        database="database",
                        charset="charset") as connection:
        pass

def test_connect_mysql(mysql_connection):
    print("\n连接MySQL数据库")

3.5.3 使用addfinalizer方法

    fixture函数可以接收一个request参数,表示测试请求的上下文。可以使用request.addfinalizer方法为fixture添加清理销毁函数,示例如下所示:

# @IDE:      PyCharm
# @Project:  PyCharmProjects
# @File:     test_fixture_06.py
# @Time      2025-03-11 23:39
# @Author:   Surpass Lee
# @E-mail:   surpassmebyme@gmail.com

import pytest

@pytest.fixture()
def add_finalizer_fixture(request):
    print("\n【fixture】 在每个用例前执行一次")

    def teardown_finalizer():
        print("\n【fixture】 在每个用例执行完成后,执行一次")

    # 注册teardown_finalizer为销毁函数
    request.addfinalizer(teardown_finalizer)


def test_case_01(add_finalizer_fixture):
    print("\n:01-执行第一用例")

def test_case_02(add_finalizer_fixture):
    print("\n:01-执行第二用例")

    运行结果如下所示:

============================= test session starts =============================
collecting ... collected 2 items

test_fixture_06.py::test_case_01 
【fixture】 在每个用例前执行一次
PASSED                                  [ 50%]
:01-执行第一用例

【fixture】 在每个用例执行完成后,执行一次

test_fixture_06.py::test_case_02 
【fixture】 在每个用例前执行一次
PASSED                                  [100%]
:01-执行第二用例

【fixture】 在每个用例执行完成后,执行一次


============================== 2 passed in 0.02s ==============================

如果在yield或addfinalizer注册之前代码发生错误并退出,则不会执行后续的销毁动作。

3.5.4 yield与addfinalizer区别

    除在使用上的区别之外,addfinalizer和yield两者的区别还有,addfinalizer可以注册多个销毁函数,而yield无法实现多个,在执行yield过程出现错误,则无法再执行后面的函数,而addfinalizer函数在执行过程即使出错也会执行后面的函数。

# @IDE:      PyCharm
# @Project:  PyCharmProjects
# @File:     test_fixture_07.py
# @Time      2025-03-11 23:50
# @Author:   Surpass Lee
# @E-mail:   surpassmebyme@gmail.com
import pytest

@pytest.fixture()
def add_finalizer_fixture(request):
    print("\n【fixture】 在每个用例前执行一次")

    def teardown_finalizer_01():
        print("\n【teardown_finalizer_01】 在每个用例执行完成后,执行一次")

    def teardown_finalizer_02():
        print("\n【teardown_finalizer_02】 在每个用例执行完成后,执行一次")

    def teardown_finalizer_03():
        print("\n【teardown_finalizer_03】 在每个用例执行完成后,执行一次")

    # 注册teardown_finalizer为销毁函数
    finalizer_list=[teardown_finalizer_01,teardown_finalizer_02,teardown_finalizer_03]
    for item in finalizer_list:
        request.addfinalizer(item)

def test_case_01(add_finalizer_fixture):
    print("\n:01-执行第一用例")

def test_case_02(add_finalizer_fixture):
    print("\n:01-执行第二用例")

    运行结果如下所示:

collecting ... collected 2 items

test_fixture_07.py::test_case_01 
【fixture】 在每个用例前执行一次
PASSED                                  [ 50%]
:01-执行第一用例

【teardown_finalizer_03】 在每个用例执行完成后,执行一次

【teardown_finalizer_02】 在每个用例执行完成后,执行一次

【teardown_finalizer_01】 在每个用例执行完成后,执行一次

test_fixture_07.py::test_case_02 
【fixture】 在每个用例前执行一次
PASSED                                  [100%]
:01-执行第二用例

【teardown_finalizer_03】 在每个用例执行完成后,执行一次

【teardown_finalizer_02】 在每个用例执行完成后,执行一次

【teardown_finalizer_01】 在每个用例执行完成后,执行一次

通过 request.addfinalizer() 注册的销毁函数均执行,但执行顺序与注册顺序刚好相反

3.6 fixture 应用层级

    在使用fixture时,可以添加scope参数,用于控制该fixture的作用范围。主要范围有:

  • function: 每个函数或方法都会调用该fixture
  • class: 每个类调用一次
  • module:每个模块可以调用一次
  • session: 多个文件调用一次

如果fixture应用层级为session,则需要将fixture写在conftest.py文件中

3.7 使用params 传递数据

    在使用fixture,除之前的参数scope之外,还可以使用params传递参数,request.param获取传递参数。其应用场景为,在执行测试用例时,每轮执行都是同一个fixture,但却有不同的依赖场景,则可以使用参数化来应对这种场景。

3.7.1 常规使用方法

# @IDE:      PyCharm
# @Project:  PyCharmProjects
# @File:     test_fixture_08.py
# @Time      2025-03-12 0:05
# @Author:   Surpass Lee
# @E-mail:   surpassmebyme@gmail.com
import pytest


@pytest.fixture(params=["surpass","kevin","ben"])
def person(request):
    # 将fixture中的params依次返回
    return request.param

def test_get_person(person):
    print(f"\n当前人员为:{person}")
    assert  2 == 2

    运行结果如下所示:

collecting ... collected 3 items

test_fixture_08.py::test_get_person[surpass] PASSED                      [ 33%]
当前人员为:surpass

test_fixture_08.py::test_get_person[kevin] PASSED                        [ 66%]
当前人员为:kevin

test_fixture_08.py::test_get_person[ben] PASSED                          [100%]
当前人员为:ben

3.7.2 xfail 测试数据

    在测试某些功能时,如果步骤一致,则可以使用数据驱动方式进行测试,即一个测试方法使用多个数据。通常正确有效的数据使用一个测试方法,错误的数据使用一个测试方法,实行分开验证。那有没有一种办法,可以将这两者组合到一起来测试呢?这种情况下可以使用pytest.param标记预期失败。

# @IDE:      PyCharm
# @Project:  PyCharmProjects
# @File:     test_fixture_09.py
# @Time      2025-03-12 0:13
# @Author:   Surpass Lee
# @E-mail:   surpassmebyme@gmail.com

import pytest

@pytest.fixture(params=[
                        ("1+2",3),
                        pytest.param(("3+4",8),
                                    marks=pytest.mark.xfail,
                                    id="xfail")
                ])
def compute_data(request):
    return request.param

def test_computer_data(compute_data):
    expression,expect=compute_data
    assert eval(expression) == expect

    运行结果如下所示:

collecting ... collected 2 items

test_fixture_09.py::test_computer_data[compute_data0] 
test_fixture_09.py::test_computer_data[xfail] 

======================== 1 passed, 1 xfailed in 0.05s =========================
PASSED             [ 50%]XFAIL                      [100%]
compute_data = ('3+4', 8)

    def test_computer_data(compute_data):
        expression,expect=compute_data
>       assert eval(expression) == expect
E       AssertionError: assert 7 == 8
E        +  where 7 = eval('3+4')

test_fixture_09.py:21: AssertionError

3.7.3 params 与ids

  &emspp; 对于复杂类型的测试数据通常会加上id或name来表明数据的含义,并标记测试要点。测试数据允许字符串、表达式、元组、字典等类型。pytest使用ids关键字参数,自定义测试ID。示例代码如下所示:

# @IDE:      PyCharm
# @Project:  PyCharmProjects
# @File:     test_fixture_10.py
# @Time      2025-03-12 16:20
# @Author:   Surpass Lee
# @E-mail:   surpassmebyme@gmail.com

import pytest

@pytest.fixture(params=[0,"surpass",(1,2),{"name":"surpass"}],ids=["int","str","tuple","dict"])
def data_01(request):
    return request.param

def test_data_01(data_01):
    print(f"\n获取到的数据为{data_01}")

def id_fun(fixture_value):
    if isinstance(fixture_value,(int)):
        return "number"
    elif isinstance(fixture_value,(tuple)):
        return "tuple"
    elif isinstance(fixture_value,(dict)):
        return  "dict"
    elif isinstance(fixture_value,(str,)):
        return "str"
    else:
        return "other"

@pytest.fixture(params=[0,"surpass",(1,2),{"name":"surpass"},[1,2]],ids=id_fun)
def data_02(request):
    return  request.param

def test_data_02(data_02):
    print(f"\n获取到的数据为{data_02}")

class MyIds():
    pass

@pytest.fixture(params=[(1,2),{"name":"surpass"},MyIds()])
def data_03(request):
    return request.param

def test_data_03(data_03):
    print(f"\n获取到的数据为{data_03}")

    运行结果如下所示:

============================= test session starts =============================
collecting ... collected 12 items

test_fixture_10.py::test_data_01[int] PASSED                             [  8%]
获取到的数据为0

test_fixture_10.py::test_data_01[str] PASSED                             [ 16%]
获取到的数据为surpass

test_fixture_10.py::test_data_01[tuple] PASSED                           [ 25%]
获取到的数据为(1, 2)

test_fixture_10.py::test_data_01[dict] PASSED                            [ 33%]
获取到的数据为{'name': 'surpass'}

test_fixture_10.py::test_data_02[number] PASSED                          [ 41%]
获取到的数据为0

test_fixture_10.py::test_data_02[str] PASSED                             [ 50%]
获取到的数据为surpass

test_fixture_10.py::test_data_02[tuple] PASSED                           [ 58%]
获取到的数据为(1, 2)

test_fixture_10.py::test_data_02[dict] PASSED                            [ 66%]
获取到的数据为{'name': 'surpass'}

test_fixture_10.py::test_data_02[other] PASSED                           [ 75%]
获取到的数据为[1, 2]

test_fixture_10.py::test_data_03[data_030] PASSED                        [ 83%]
获取到的数据为(1, 2)

test_fixture_10.py::test_data_03[data_031] PASSED                        [ 91%]
获取到的数据为{'name': 'surpass'}

test_fixture_10.py::test_data_03[data_032] PASSED                        [100%]
获取到的数据为<test_fixture_10.MyIds object at 0x0000018EB349C200>


============================= 12 passed in 0.03s ==============================

    从执行结果可以看出:ids可以接收一个函数或一个列表,用于生成测试ID。当测试ID未指定时,使用params原先对应的值。

3.8 自动调用fixture

    如果每次使用fixture都要通过传参的方式,则会改变原来测试方法的结构。那有没有在不改变测试方法的前提下,直接通过注入方式执行呢?pytest提供了两种方式:

  • 设置fixture中的参数autouse=true
  • 在测试方法中使用@pytest.mark.usefixtures

3.8.1 使用参数autouse=true

    示例代码如下所示:

# @IDE:      PyCharm
# @Project:  PyCharmProjects
# @File:     test_fixture_11.py
# @Time      2025-03-12 18:54
# @Author:   Surpass Lee
# @E-mail:   surpassmebyme@gmail.com

import pytest

@pytest.fixture(autouse=True)
def login():
    print("\n用户登录")

def test_add_user():
    print("\n添加用户")

def test_del_user():
    print("\n删除用户")

def test_logout():
    print("\n退出登录")

    代码运行结果如下所示:

============================= test session starts =============================
collecting ... collected 3 items

test_fixture_11.py::test_add_user 
用户登录
PASSED                                 [ 33%]
添加用户

test_fixture_11.py::test_del_user 
用户登录
PASSED                                 [ 66%]
删除用户

test_fixture_11.py::test_logout 
用户登录
PASSED                                   [100%]
退出登录


============================== 3 passed in 0.02s ==============================

因在pytest.fixture中未指定scope,则默认应用于每个测试方法。

3.8.2 使用@pytest.mark.usefixtures

    如果使用@pytest.fixture(autouse=Ture),则代表所有测试方法均会使用,而有时候仅部分测试方法需要使用fixture时,则可以使用@pytest.mark.usefixtures来实现,示例代码如下所示:

# @IDE:      PyCharm
# @Project:  PyCharmProjects
# @File:     test_fixture_12.py
# @Time      2025-03-12 20:38
# @Author:   Surpass Lee
# @E-mail:   surpassmebyme@gmail.com

import pytest

@pytest.fixture(scope="function")
def data_01():
    print("\n 01:调用fixture data_01")
    return {"name":"Surpass","location":"Shanghai"}

@pytest.fixture(scope="function")
def data_02():
    print("\n 02:调用fixture data_02")
    return ("Surpass","Shanghai")

@pytest.mark.usefixtures("data_01")
@pytest.mark.usefixtures("data_02")
def test_data_01():
    print(f"\n 运行测试方法-01")
    assert 1 == 1

def test_data_02():
    print(f"\n 运行测试方法-02")
    assert 1 == 1

    运行结果如下所示:

============================= test session starts =============================
collecting ... collected 2 items

test_fixture_12.py::test_data_01 
 02:调用fixture data_02

 01:调用fixture data_01
PASSED                                  [ 50%]
 运行测试方法-01

test_fixture_12.py::test_data_02 PASSED                                  [100%]
 运行测试方法-02


============================== 2 passed in 0.02s ==============================

3.9 fixture 使用

    相互依赖的fixture存在多个或者有多个fixture之间存在先后依赖关系等场景,我们需要结合不同的场景来使用合适的fixture.

3.9.1 fixture 并列使用

    当fixture存在多个,可以并列使用fixture,示例代码如下所示:

# @IDE:      PyCharm
# @Project:  PyCharmProjects
# @File:     test_fixture_13.py
# @Time      2025-03-12 20:55
# @Author:   Surpass Lee
# @E-mail:   surpassmebyme@gmail.com

import pytest


@pytest.fixture(scope="module")
def login():
    print("\n01:用户登录")


@pytest.fixture()
def add_user():
    print("\n02:添加用户")


@pytest.fixture()
def del_user():
    print("\n03:删除用户")


def test_search_user_01(login, del_user, add_user):
    print("\ncase-01:搜索用户")


@pytest.mark.usefixtures("add_user")
@pytest.mark.usefixtures("del_user")
@pytest.mark.usefixtures("login")
def test_search_user_02(login, del_user, add_user):
    print("\ncase-02:搜索用户")

    运行结果如下所示:

============================= test session starts =============================
collecting ... collected 2 items

test_fixture_13.py::test_search_user_01 
01:用户登录

03:删除用户

02:添加用户
PASSED                           [ 50%]
case-01:搜索用户

test_fixture_13.py::test_search_user_02 
03:删除用户

02:添加用户
PASSED                           [100%]
case-02:搜索用户


============================== 2 passed in 0.01s ==============================

从结果可以看出fixture login的scope为module,所以仅在module中执行一次。而fixture del_user 和 add_user 的scope为function,属于同级别,所以按传入的参数顺序,在每个测试方法都执行一次。

3.9.2 fixture 嵌套

    fixture嵌套主要应用场景为多个fixture存在先后关系,示例代码如下所示:

# @IDE:      PyCharm
# @Project:  PyCharmProjects
# @File:     test_fixture_14.py
# @Time      2025-03-12 21:07
# @Author:   Surpass Lee
# @E-mail:   surpassmebyme@gmail.com
import pytest

@pytest.fixture()
def login_data():
    print("\n00:返回登录用户名和密码")
    return {"username":"Surpass","passwd":"passwd"}

@pytest.fixture()
def login(login_data):
    print("\n01:用户登录")

@pytest.fixture()
def del_user(login):
    print("\n03:删除用户")

@pytest.fixture()
def add_user(del_user):
    print("\n02:添加用户")

def test_search_user_01(add_user):
    print("\ncase-01:搜索用户")

    运行结果如下所示:

============================= test session starts =============================
collecting ... collected 1 item

test_fixture_14.py::test_search_user_01 
00:返回登录用户名和密码

01:用户登录

03:删除用户

02:添加用户
PASSED                           [100%]
case-01:搜索用户


============================== 1 passed in 0.02s ==============================

3.9.3 fixture 实例化顺序

    在pytest中可以允许存在多个fixture,而且也允许存在并列、嵌套关系、不同的作用域等。因此多个fixture时,需要了解其实例化顺序

  • 高级别作用域的实例化高于低级别作用域的fixture,例如session的fixture要高于function
  • 相同级别作用域的实例化,其实例化顺序要遵循fixture在测试方法中声明的顺序,即传入参数的顺序,如果使用@pytest.mark.usefixture,则优先实例化离测试方法最近的fixture
  • autouse=True的fixture实例化要高于同级别的fixture

3.10 fixture 重写

    通常在大型测试项目中,可能需要在本地覆盖项目级别的fixture,以增加可读性和可维护性。即可以通过在不同层级中重写fixture实现改变fixture中原始内容,思路是重写覆盖同名方法,在使用时优选调用重写后的fixture。在module层级重写fixture,可以新建conftest.py文件并在该文件中写fixture,测试方法在使用fixture,优先从同级目录搜索相应的fixture

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容