前言
这篇文章主要探究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
正常执行,而test02
和test_03
都使用了模块级别的register
,发现test02
和test_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 2
def 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
等,笔者工作中暂时没有用到,等用到时再做介绍。