掌握 pytest fixture:优化你的测试代码(二)

271 阅读6分钟

前言

这篇文章主要探究fixture的并列嵌套使用以及重写fixture

并列使用fixture

实用场景:比如我们需要有多个前置条件。看下面例子

import pytest
​
@pytest.fixture()
def register():
    print("注册成功")
​
@pytest.fixture()
def login():
    print("登录成功")
​
​
def test_01(register, login):
    print("test_01执行")
​

由于两个fixture的scope都是function,执行顺序依次执行:注册成功->登录成功->test_01执行

当然也可使用这篇文章中提到的其他方法实现,比如下面这样

@pytest.mark.usefixtures("register","login")
def test_01():
    print("test_01执行")

可以达到相同的效果。

注意:如果使用autouse方式实现,在作用域相同的情况下,执行顺序可能不满足你的要求,像下面这样

import pytest
​
@pytest.fixture(autouse=True)
def register():
    print("注册成功")
​
@pytest.fixture(autouse=True)
def login():
    print("登录成功")
​
​
def test_01():
    print("test_01执行")

输出结果为:登录成功->注册成功->test_01执行,不满足正常业务逻辑。

嵌套调用fixture

使用场景:有先后关系的调用。比如上面的代码,注册登录,我们是通过传两个参数来实现。当然也可以优化,变为下面这样

import pytest
​
@pytest.fixture()
def register():
    print("注册成功")
​
@pytest.fixture()
def login(register):
    print("登录成功")
​
​
def test_01(login):
    print("test_01执行")

优化之后,只需要在测试方法中传入login即可调用注册、登录。

不同作用域的fixture调用顺序

  • 高级别作用域先于低级别作用域;
  • 相同级别作用域,顺序遵循它们在测试用例中被声明的顺序,也就是形参的顺序,或者fixture之间的相互调用关系;
  • 自动应用autouse的fixture,先于其同级别的其他fixture实例化。

看下面代码,验证结论

import pytest
​
@pytest.fixture(scope="session")
def scope_session():
    print("session执行")
​
@pytest.fixture(scope="module")
def scope_module():
    print("module执行")
​
@pytest.fixture()
def scope_function():
    print("function执行")
​
@pytest.fixture(autouse=True)
def autouse_function():
    print("autouse_function执行")
​
def test_01(scope_function, scope_module, scope_session):
    print("test_01执行")
​

为了验证结论,传参顺序我们安照作用域由低到高入参,执行结果:session执行->module执行->autouse_function执行->function执行

多个相同作用域的autouse fixture,顺序遵循fixture函数名的排序,怎理解呢?举个例子

import pytest
​
@pytest.fixture(autouse=True)
def function_b():
    print("function_b执行")
​
@pytest.fixture(autouse=True)
def function_a():
    print("function_a执行")
​
def test_01():
    print("test_01执行")

像这样,fixture 函数名是字符串类型,它们将按照字母顺序进行排序。"function_a" 将在 "function_b" 之前执行。

fixture返回工厂函数

实用场景:当一个用例中,多次使用同一个fixture时。看下面这个例子

import pytest
​
@pytest.fixture()
def user_factory():
    users = []
​
    def create_user(username):
        print(f"创建用户:{username}")
        user = {"username": username}
        users.append(user)
        return user
​
    yield create_user
​
    # 在 fixture 执行完后进行清理操作
    print("清理操作")
    users.clear()
​
def test_create_user(user_factory):
    user1 = user_factory("Alice")
    assert user1["username"] == "Alice"
​
    user2 = user_factory("Bob")
    assert user2["username"] == "Bob"

我们使用 fixture 返回一个工厂函数,可以通过在测试用例中调用该 fixture 来获取返回的实际对象。这种方式非常灵活,可以根据需要动态生成测试数据或对象。

参数化fixture

import pytest
​
@pytest.fixture(params = [1,2])
def login(request):
    param = request.param
    print(f"用户{param}登录")
    yield param
    print(f"用户{param}退出")
​
@pytest.fixture(scope="module", params = [1,2])
def register(request):
    param = request.param
    print(f"用户{param}注册")
    yield param
    print(f"用户{param}注销账号")
​
def test_01(login):
    print("test_01执行")
​
def test_02(register):
    print("test_02执行")
​
def test_03(login, register):
    print("test_03执行")

代码很容易理解,就是使用params参数,重点是执行结果可能出乎意料。

没运行执行,我现在运行结果是这样的

用户1登录
test_01执行
用户1退出
用户2登录
test_01执行
用户2退出
用户1注册
test_02执行
用户1注销账号
用户2注册
test_02执行
用户2注销账号
用户1注册
用户1登录
test_03执行
用户1退出
用户2登录
test_03执行
用户2退出
用户1注销账号
用户2注册
用户1登录
test_03执行
用户1退出
用户2登录
test_03执行
用户2退出
用户2注销账号
​

实际运行之后是这样的

用户1登录
test_01执行
用户1退出
用户2登录
test_01执行
用户2退出
用户1注册
test_02执行
用户1登录
test_03执行
用户1退出
用户2登录
test_03执行
用户2退出
用户1注销账号
用户2注册
test_02执行
​
用户1登录
test_03执行
用户1退出
用户2登录
test_03执行
用户2退出
用户2注销账号
​

这段代码,定义了两个参数化的fixture,其中一个是模块级别的作用域,另一个是用例级别的作用域。执行后发现,test_01正常执行,而test02test_03都使用了模块级别的register,发现test02test_03共用相同的register,高效利用fixture实例,最少化的保留fixture的实例个数

重写fixture

conftest.py层级重写

新建文件pxl/conftest.py

import pytest
​
​
@pytest.fixture
def login():
    print("登录成功")
    return 1

继续新建文件pxl/test_dir/conftest.py

import pytest
​
​
@pytest.fixture
def login(login):
    print("重写登录,登录成功")
    return 2

可以看到,参数调用上一级conftest.py中的login,通过同名的方法重写上一级的同名fixture方法。

新建文件pxl/test_dir/test_demo.py进行测试

def test_01(login):
    print("test_01执行")
    assert login == 2

执行发现,调用的是重写后的login

模块层级重写

新建文件pxl/conftest.py

import pytest
​
​
@pytest.fixture
def login():
    print("登录成功")
    return 1

继续新建文件pxl/test_dir/test_demo.py

import pytest
​
@pytest.fixture
def login(login):
    print("重写登录,登录成功")
    return 2def test_01(login):
    print("test_01执行")
    assert login == 2

执行结果:登录成功->重写登录,登录成功->test_01执行

可以看到,模块(文件)中的fixture可以轻松地访问conftest.py中同名的fixture。

用例参数中重写

使用 pytest.mark.parametrize 标记来覆盖 fixture 中的参数化测试。这样可以在测试用例中直接指定不同的参数值,而不必依赖于 fixture 的参数化。

import pytest
​
@pytest.fixture(params=[1, 2, 3])
def data(request):
    return request.param
​
@pytest.mark.parametrize("data", [10, 20, 30])
def test_data(data):
    assert data in [10, 20, 30]
​

可以看到,data fixture 的参数化会运行三次,分别使用参数值 1、2、3。但是,想覆盖这个参数化,可以在测试用例中使用 pytest.mark.parametrize 标记,这样每次使用参数值 10、20、30

非参数化fixture覆盖参数化fixture,反过来也可以

非参数化fixture覆盖参数化fixture

新建文件pxl/conftest.py

import pytest
​
@pytest.fixture(params=[1, 2, 3])
def data(request):
    return request.param
​
@pytest.fixture
def non_param_data():
    return 10

继续新建文件pxl/test_dir/test_demo.py

import pytest
​
@pytest.fixture()
def data():
    return 10@pytest.fixture(params=[1, 2, 3])
def non_param_data(request):
    return request.param
​
​
def test_data(data, non_param_data):
    assert data == 10
    assert non_param_data in [1, 2, 3]

在test_demo.py文件中,创建与conftest.py文件中同名的data方法,重写为只返回一个值的非参数化方法。创建与conftest.py文件中同名的non_param_data方法,添加参数及返回值,重写为参数化的方法。

最后

fixture的主要常用功能就介绍完了,当然还有很多功能,像动态作用域、使用其他项目的fixture等,笔者工作中暂时没有用到,等用到时再做介绍。