什么是固定装置
在深入探讨什么是固定装置之前,让我们先看看什么是测试。
用最简单的术语来说,测试旨在查看特定行为的结果,并确保该结果与您的预期一致。行为不是可以凭经验衡量的,这就是编写测试具有挑战性的原因。
“行为”是某些系统响应特定情况和/或刺激而采取行动的方式。但究竟如何或为什么做某事并不像做了什么那么重要。
您可以将测试分为四个步骤:
安排
行为
断言
清理
安排是我们为测试准备一切的地方。这意味着除了“行为”之外的几乎所有内容。它正在排列多米诺骨牌,以便该行为可以在一个改变状态的步骤中完成它的事情。这可能意味着准备对象、启动/终止服务、将记录输入数据库,甚至是定义要查询的 URL、为尚不存在的用户生成一些凭据或只是等待某个过程完成之类的事情。
法是单数,状态变化的行动揭开序幕的行为, 我们希望测试。这种行为是执行被测系统 (SUT) 状态更改的原因,并且我们可以查看由此产生的更改状态来判断该行为。这通常采用函数/方法调用的形式。
Assert是我们查看结果状态的地方,并检查它是否看起来像我们在尘埃落定后所期望的那样。这是我们收集证据的地方,以说明行为是否符合我们的预期。在assert我们的测试是我们采取的测量/观察,并应用我们的判断吧。如果某些东西应该是绿色的,我们会说。assert thing == “green”
清理是测试在其自身之后进行的地方,因此其他测试不会意外地受到它的影响。
在它的核心,测试最终是行为和断言步骤, 安排步骤只提供上下文。行为存在于act 和assert之间。
“夹具”,从字面意义上来说,就是每一个排列步骤和数据。它们是测试需要做的一切。
在基本级别上,测试函数通过将夹具声明为参数来请求它们
在 pytest 中,“fixtures”是您定义的用于此目的的函数。但它们不必仅限于排列步骤。它们也可以提供 act步骤,这对于设计更复杂的测试来说是一种强大的技术,特别是考虑到 pytest 的夹具系统是如何工作的。但我们将进一步深入探讨。
我们可以通过用 @pytest.fixture. 这是一个简单的例子,说明 pytest 中的固定装置可能是什么样子:
import pytest
class Fruit:
def __init__(self, name):
self.name = name
def __eq__(self, other):
return self.name == other.name
@pytest.fixture
def my_fruit():
return Fruit("apple")
@pytest.fixture
def fruit_basket(my_fruit):
return [Fruit("banana"), my_fruit]
def test_my_fruit_in_basket(my_fruit, fruit_basket):
assert my_fruit in fruit_basket
测试也不必局限于单个夹具。它们可以依赖任意数量的灯具,灯具也可以使用其他灯具。这就是 pytest 的夹具系统真正大放异彩的地方。
如果它使事情变得更清洁,请不要害怕打破事情。
“请求”固定装置
因此,fixture 是我们为测试做准备的方式,但是我们如何告诉 pytest 哪些测试和fixture 需要哪些fixture?
在基本级别上,测试函数通过将夹具声明为参数来请求它们,如前面的示例中所示。test_my_fruit_in_basket(my_fruit, fruit_basket):
在基本层面上,pytest 依赖于一个测试来告诉它它需要什么夹具,所以我们必须将该信息构建到测试本身中。我们必须让测试“请求”它所依赖的fixture,为此,我们必须在测试函数的“signature”(即行)中将这些fixture 作为参数列出。def test_something(blah, stuff, more):
当 pytest 开始运行测试时,它会查看该测试函数签名中的参数,然后搜索与这些参数具有相同名称的夹具。一旦 pytest 找到它们,它就会运行这些固定装置,捕获它们返回的内容(如果有的话),并将这些对象作为参数传递给测试函数。
快速示例
import pytest
class Fruit:
def __init__(self, name):
self.name = name
self.cubed = False
def cube(self):
self.cubed = True
class FruitSalad:
def __init__(self, *fruit_bowl):
self.fruit = fruit_bowl
self._cube_fruit()
def _cube_fruit(self):
for fruit in self.fruit:
fruit.cube()
# Arrange
@pytest.fixture
def fruit_bowl():
return [Fruit("apple"), Fruit("banana")]
def test_fruit_salad(fruit_bowl):
# Act
fruit_salad = FruitSalad(*fruit_bowl)
# Assert
assert all(fruit.cubed for fruit in fruit_salad.fruit)
在这个例子中,test_fruit_salad“ requests ” fruit_bowl(即 ),当 pytest 看到这个时,它将执行夹具函数并将它返回的对象作为参数传递
场景: 测试⽤例执⾏时,有的⽤例需要登陆才能执⾏,有些⽤例不需要登陆。setup 和 teardown ⽆法满⾜。fixture 可以。默认 scope(范围)function
步骤:
1.导⼊ pytest
2.在登陆的函数上⾯加@pytest.fixture()
3.在要使⽤的测试⽅法中传⼊(登陆函数名称),就先登陆
4.不传⼊的就不登陆直接执⾏测试⽅法。
引用fixture有两种方式:
"""
pytest之fixture
"""
import pytest
@pytest.fixture()
def login():
print("登录成功")
return "token"
# 第一种引用方式:直接将函数名当成参数传递
def test_fix(login):
print("接口返回token:", login)
# 第二种引用方式:使用pytest.mark.usefixtures
@pytest.mark.usefixtures("login")
def test_two():
print(login)
def test_thr():
print("test 3")
运行结果:
E:\Home_Work\Home_Work2\pytest01\test>pytest test_fixture.py
collected 3 items
test_fixture.py::test_fix 登录成功
接口返回token: token
PASSED
test_fixture.py::test_two 登录成功
<function login at 0x000001D3A958CAF0>
PASSED
test_fixture.py::test_thr test 3
PASSED
========================================================= 3 passed in 0.39s =========================================================
E:\Home_Work\Home_Work2\pytest01\test>
装饰器标记一个夹具工厂功能。
这个装饰器可以用来定义一个fixture函数,带或不带参数 。
以后可以引用夹具函数的名称以在运行测试之前调用它:测试模块或类可以使用该 pytest.mark.usefixtures(fixturename)标记。
测试函数可以直接使用夹具名称作为输入参数,在这种情况下,夹具函数返回的夹具实例将被注入。
Fixture可以使用returnor yield语句提供它们的值来测试函数。当使用语句yield后的代码块 yield作为拆卸代码执行时,无论测试结果如何,并且必须只产生一次。
参数
fixture源码:
def fixture(
fixture_function: Optional[_FixtureFunction] = None,
*,
scope: "Union[_Scope, Callable[[str, Config], _Scope]]" = "function",
params: Optional[Iterable[object]] = None,
autouse: bool = False,
ids: Optional[
Union[
Iterable[Union[None, str, float, int, bool]],
Callable[[Any], Optional[object]],
]
] = None,
name: Optional[str] = None,
) -> Union[FixtureFunctionMarker, _FixtureFunction]:
scope : 范围——
共享此夹具的范围;之一"function" (默认), ,,或。“class”“module”“package”“session”
此参数也可以是 作为参数接收的可调用对象,并且必须返回具有上述值之一的a 。(fixture_name, config)str
params – 一个可选的参数列表,它将导致对夹具函数和所有使用它的测试的多次调用。当前参数在 中可用request.param。
autouse – 如果为 True,则为所有可以看到它的测试激活夹具func。如果为 False(默认值),则需要显式引用来激活夹具。
ids – 每个对应于参数的字符串 id 列表,因此它们是测试 id 的一部分。如果没有提供 id,它们将从参数中自动生成。
name -夹具的名称。这默认为装饰函数的名称。如果固定装置在其被定义在相同的模块中使用的,功能名称夹具将被函数ARG请求固定装置被遮蔽; 解决此问题的一种方法是命名装饰函数fixture_,然后使用 @pytest.fixture(name=’’).
scope: 表示fixture共享夹具的范围,有以下五个范围
package 与 session 通常结合conftest.py 进行使用。
module 级别 (等价于pytest与testcase框架的setup_class、teardown_class机制)autouse=True表示当前所有的测试用例都生效
import pytest
@pytest.fixture(scope="class", autouse=True)
def login():
print("登录")
yield "token:1045645343565sldjhncundm"
print("登出")
class TestCase:
def test_01(self):
print("test 001")
def test_02(self):
print("test 002")
运行结果:
>pytest test_yield.py
collected 2 items
test_yield.py::TestCase::test_01 登录
test 001
PASSED
test_yield.py::TestCase::test_02 test 002
PASSED登出
========================================================= 2 passed in 0.67s =========================================================
E:\Home_Work\Home_Work2\pytest01\test>
function 级别
# -*- coding = utf-8 -*-
# @time:2022/1/8 11:36
# Author:Leo
# @File:test_yield.py
# @Software:PyCharm
"""
使用pytest fixtrue 实现 setup 和 teardown
"""
import pytest
@pytest.fixture(scope="function", autouse=True)
def login():
print("登录")
yield "token:1045645343565sldjhncundm"
print("登出")
class TestCase:
def test_01(self):
print("test 001")
def test_02(self):
print("test 002")
运行结果:
>pytest test_yield.py
collected 2 items
test_yield.py::TestCase::test_01 登录
test 001
PASSED登出
test_yield.py::TestCase::test_02 登录
test 002
PASSED登出
========================================================= 2 passed in 0.49s =========================================================
E:\Home_Work\Home_Work2\pytest01\test>
使用yield实现了setup和teardown底层逻辑:
在上面的例子中我们在yield前后各加入了登录与登出,然后分别在测试用例的前后执行了,从而实现了 setup 和 teardown 的功能,我们可以用 pytest test_yield.py --setup-show 打印出具体的执行步骤,如下:
E:\Home_Work\Home_Work2\pytest01\test>pytest test_yield.py --setup-show
======================================================== test session starts ========================================================
collected 2 items
test_yield.py::TestCase::test_01
SETUP S _session_faker登录
SETUP F login
test/test_yield.py::TestCase::test_01 (fixtures used: _session_faker, login, request)test 001
PASSED登出
TEARDOWN F login
test_yield.py::TestCase::test_02 登录
SETUP F login
test/test_yield.py::TestCase::test_02 (fixtures used: _session_faker, login, request)test 002
PASSED登出
TEARDOWN F login
TEARDOWN S _session_faker
========================================================= 2 passed in 0.63s =========================================================
E:\Home_Work\Home_Work2\pytest01\test>pytest test_yield.py --setup-show
根据执行步骤我们可以发现,每次在执行一次用例的时候就会执行两次login(fixture),第一次是实现了setup的功能,即yield前面的功能。第二次执行yield后面的功能,从而实现teardown的功能。
fixtures used: _session_faker, login, request 表示当前有3个fixture,其中login使我们自己定义的,另外两个是pytest fixture自带的fixture,所以fixture是可以调用其他fixture,这个后面讲。
场景: 你与其他测试⼯程师合作⼀起开发时,公共的模块要在不同⽂件中,要在⼤家都访问到的地⽅。
解决: 使⽤ conftest.py 这个⽂件进⾏数据共享,并且他可以放在不同位置起着不同的范围共享作⽤。
前提:
conftest ⽂件名是不能换的
放在项⽬下是全局的数据共享的地⽅
执⾏:
系统执⾏到参数 login 时先从本模块中查找是否有这个名字的变量什么的,
之后在 conftest.py 中找是否有。
步骤: 将登陆模块带@pytest.fixture 写在 conftest.py
conftest.py
"""
测试数据共享文件 conftest.py
"""
import pytest
@pytest.fixture(scope="class")
def login():
# 使用pytest fixture 实现 setup 和 teardown
print("登录")
yield "token:1045645343565sldjhncundm"
print("登出")
@pytest.fixture(scope="class")
def db_connect():
print("连接数据库")
yield
print("断开数据库")
test_yield.py
class TestCase:
def test_01(self, login, db_connect):
print("test 001")
def test_02(self, login, db_connect):
print("test 002")
运行test_yield.py 结果如下:
E:\Home_Work\Home_Work2\pytest01\test>pytest test_yield.py
collected 2 items
test_yield.py::TestCase::test_01 登录
连接数据库
test 001
PASSED
test_yield.py::TestCase::test_02 test 002
PASSED断开数据库
登出
========================================================= 2 passed in 0.46s ========================
PS:当存在多个目录中有conftest.py时,采用就近原则。
自己模块 > 当前目录下conftest.py > 父目录下conftest.py > … > / conftest.py , 层层往上,永远不会去寻找兄弟目录下的conftest.py
场景:
测试离不开数据,为了数据灵活,⼀般数据都是通过参数传的
解决:
fixture 通过固定参数 request 传递
步骤:
在 fixture 中增加@pytest.fixture(params=[1, 2, 3, ‘linda’]) 在⽅法参数写 request,方法体里面使用 request.param 接收参数
"""
fixture 实现参数化
"""
import pytest
@pytest.fixture(params=['tom', "jack", "tony"], ids=["name1", "name2", "name3"])
def demo(request):
print("登录")
yield request.param
print("登出")
def test_demo(demo):
print("用户名", demo)
运行结果:
E:\Home_Work\Home_Work2\pytest01\test>pytest test_fixture_params.py
======================================================== test session starts ========================================================
platform win32 -- Python 3.8.7, pytest-6.2.5, py-1.10.0, pluggy-0.13.1 -- C:\Users\zyd\AppData\Local\Programs\Python\Python38\python.e
xe
collected 3 items
test_fixture_params.py::test_demo[name1] 登录
用户名 tom
PASSED登出
test_fixture_params.py::test_demo[name2] 登录
用户名 jack
PASSED登出
test_fixture_params.py::test_demo[name3] 登录
用户名 tony
PASSED登出
========================================================= 3 passed in 0.61s =========================================================
E:\Home_Work\Home_Work2\pytest01\test>
我们进入fixture的源码中可以到关于 params的介绍
:param params:
An optional list of parameters which will cause multiple invocations
of the fixture function and all of the tests using it. The current
parameter is available in ``request.param``.
翻译后:
一个可选的参数列表,它将导致多个调用 夹具功能和所有使用它的测试。 当前的
参数在``request.param``中可用。
也就是说params接收一个可选的参数列表,然后通过 pytest内置的fixture request(PS:与接口自动化中的requests要区分开,不是同一个东西),通过request.param 去接收 params遍历的出每一个参数,从而实现参数化测试用例。
例子:
conftest.py
"""
测试数据共享文件 conftest.py
"""
import pytest
import yaml
@pytest.fixture(scope="class")
def login():
# 使用pytest fixture 实现 setup 和 teardown
print("登录")
yield
print("登出")
@pytest.fixture(scope="class")
def db_connect():
print("连接数据库")
yield
print("断开数据库")
def get_yaml():
# 打开一个文件,生成文件流对象
with open("./add_data.yaml") as f:
# 使用yaml解析文件流,生成一个Python可识别的数据类型
data = yaml.load(f)
print(data)
return data
@pytest.fixture(params=get_yaml()["data"], ids=get_yaml()["ids"])
def get_datas(request):
# 返回数据
data = request.param
return data
测试数据:
data:
- [ 1,2,3 ]
- [ 0.5,0.3,0.8 ]
- [ -0.1,0.1,0 ]
ids: ["int add test","float add test","int float test"]
测试用例
"""
使用fixture数据驱动
"""
# 被测对象
def add(a, b):
return a + b
# 测试脚本
class TestAdd:
def test_add(self, get_datas, db_connect, login):
print(get_datas)
a = get_datas[0]
b = get_datas[1]
expect = get_datas[2]
assert expect == add(a, b)
运行结果:
E:\Home_Work\Home_Work2\pytest01\test>pytest test_fixture_param2.py
collected 3 items
test_fixture_param2.py::TestAdd::test_add[int add test] 连接数据库
登录
[1, 2, 3]
PASSED
test_fixture_param2.py::TestAdd::test_add[float add test] [0.5, 0.3, 0.8]
PASSED
test_fixture_param2.py::TestAdd::test_add[int float test] [-0.1, 0.1, 0]
PASSED登出
断开数据库
========================================================= 3 passed in 0.61s =========================================================