关注开源优测不迷路
在编写单元测试时,是否发现自己写了很多相同/相似代码呢?
像数据库的设置与清理、API客户端或测试数据这类单调乏味的代码,如果要在几十甚至几百个单元测试中重复编写,会非常痛苦。
在编写测试时,通常需要在运行实际的测试代码之前设置一些初始状态。
编写这些设置代码可能很耗时,尤其是当有多个测试都需要相同的步骤时。
在项目的整个生命周期中,测试代码应该易于理解、重构、扩展和维护。
Pytest中的Fixtures解决了一些代码重复和样板代码的问题。
它们帮助你定义可复用的设置或清理代码,这些代码可以在多个测试中使用。
无需在每个测试中都重复相同的设置代码,只需定义一次Fixtures,就可以在多个测试中使用。
这不仅减少了代码重复,还使维护更加容易,因为任何更改都只需在一个地方进行。
在本文中,你将进一步了解Pytest Fixtures、它们的优点,以及它们如何帮助你编写更好、更简单的单元测试。
学习目标
在本教程结束时,你应该能够:
定义什么是Pytest Fixtures。
理解Pytest Fixtures的优点。
在单元测试中使用Fixtures。
理解Fixtures作用域和参数化Fixtures。
编写有效、更易于维护且利用Fixtures的单元测试。
使用Flask构建一个简单的计算器API,并使用Pytest Fixtures对其进行测试。
什么是Pytest Fixtures
在深入探讨如何应用Fixtures之前,让我们先快速了解一下Fixtures到底是什么。
Fixtures是Pytest中的一些方法,它们为测试的运行提供了一个固定的基础。
Fixtures可用于为测试设置前置条件、提供数据,或者在测试完成后执行清理操作。
在Python中,它们是使用@pytest.fixture装饰器来定义的,并且可以作为参数传递给测试函数。
Fixtures极大地简化了编写测试的过程,它允许你在多个测试中复用代码,并为每个测试提供一个一致的起点。
Pytest有几个内置的Fixtures,适用于常见的用例,例如设置测试数据库、模拟外部依赖项以及设置测试数据。
Fixtures也有各种作用域,比如函数作用域、类作用域、模块作用域和会话作用域。
Fixtures的作用域定义了在测试会话期间Fixtures的可用时长。
这使你能够控制Fixtures的生命周期,并根据你的测试需求为Fixtures选择合适的作用域。我们将在本文后面详细讨论作用域。
总的来说,Fixtures是Pytest的一个强大功能,有助于减少代码重复,提高测试的可靠性,并使测试更具模块化和可维护性。
如何使用Pytest Fixtures
现在让我们进入本文的重点部分:如何通过创建一个真正简单的应用程序来使用Fixtures。
为了理解Fixtures,我们将构建一个基本的计算器应用程序,并使用Flask API来提供服务。
如果你不熟悉Flask,别担心,你可以跳过API及其测试部分,只关注核心逻辑(计算器部分)。
项目设置
按以下项目结构构建本文的测试代码组织:

源代码
这个示例的源代码是一个Flask API,它包含一个基本的计算器应用程序,可以计算两个数字的和、差、积和商。
计算器 /calculator/core.py
go
class Calculator:
def__init__(self, a: int | float = None, b: int | float = None) -> None:
self.a = a
self.b = b
defadd(self) -> int | float:
"""
计算两个数字的和
返回:两个数字的和
"""
return self.a + self.b
defsubtract(self) -> int | float:
"""
计算两个数字的差
返回:两个数字的差
"""
return self.a - self.b
defmultiply(self) -> int | float:
"""
计算两个数字的积
返回:两个数字的积
"""
return self.a * self.b
defdivide(self) -> int | float:
"""
计算两个数字的商
返回:两个数字的商
"""
if self.b != 0:
return self.a / self.b
else:
raise ZeroDivisionError("除数不能为零")
defsquare(self) -> int | float:
"""
计算一个数字的平方
返回:一个数字的平方
"""
return self.a**2
上面是一个非常简单的Calculator类,用于执行基本的计算操作。
Flask应用程序
这个Flask应用程序为计算器提供了一个API包装器,允许我们向服务器发送远程请求并获取计算结果。
Flask应用程序如下所示:
/app/app.py
go
from flask import Flask, jsonify, request
from calculator.core import Calculator
# 创建Flask应用程序
app = Flask(__name__)
# 创建路由
@app.route('/')
defindex():
return'Index Page'
# 为加法函数添加一个路由
@app.route('/api/add/', methods=['POST'])
defadd():
data = request.get_json()
a = data['a']
b = data['b']
result = Calculator(a, b).add()
return jsonify(result)
# 为减法函数添加一个路由
@app.route('/api/subtract/', methods=['POST'])
defsubtract():
data = request.get_json()
a = data['a']
b = data['b']
result = Calculator(a, b).subtract()
return jsonify(result)
# 为乘法函数添加一个路由
@app.route('/api/multiply/', methods=['POST'])
defmultiply():
data = request.get_json()
a = data['a']
b = data['b']
result = Calculator(a, b).multiply()
return jsonify(result)
# 为除法函数添加一个路由
@app.route('/api/divide/', methods=['POST'])
defdivide():
data = request.get_json()
a = data['a']
b = data['b']
if b == 0:
return jsonify("除数不能为零"), 400
result = Calculator(a, b).divide()
return jsonify(result)
if __name__ == '__main__':
app.run()
我们有4个简单的路由(每个操作对应一个),每个路由都接受一个POST请求,请求负载中包含两个值a和b。
这个应用程序调用上面的Calculator类来执行计算。
单元测试
单元测试定义在两个单独的文件中:
test_calculator_class.py------测试Calculator类。 test_calculator_api.py------使用自定义负载测试API端点。
让我们来看看每个文件,以及它们是如何使用Pytest Fixtures的。
测试中的Fixtures
定义Fixtures最简单的方法是在测试内部进行定义。让我们看看如何使用在测试中定义的Fixtures来测试我们的代码。
test_calculator_class.py
go
import pytest
from calculator.core import Calculator
@pytest.fixture
def calculator():
return Calculator(2, 3)
使用预定义Fixtures进行基本计算器测试
go
def test_add(calculator):
assert calculator.add() == 5
deftest_subtract(calculator):
assert calculator.subtract() == -1
deftest_multiply(calculator):
assert calculator.multiply() == 6
deftest_divide(calculator):
assert calculator.divide() == 0.6666666666666666
deftest_divide_by_zero(calculator):
calculator.b = 0
with pytest.raises(ZeroDivisionError):
calculator.divide()
在这里,我们使用@pytest.fixture装饰器定义了Pytest Fixtures。
我们使用值(2, 3)初始化了Calculator类,并返回了该类的一个实例。
然后,我们将这个Fixtures传递给每个单元测试,这样就无需在每个测试中重新初始化Calculator类了。
让我们看看如何对API也进行这样的操作。
test_calculator_api.py
go
import pytest
from app.app import app
@pytest.fixture
defclient():
with app.test_client() as client:
yield client
@pytest.fixture
defjson_headers():
return {"Content-Type": "application/json"}
deftest_add(client, json_headers, json_data):
response = client.post("/api/add/", headers=json_headers, json=json_data)
assert response.status_code == 200
assert response.json == 3
deftest_subtract(client, json_headers, json_data):
response = client.post("/api/subtract/", headers=json_headers, json=json_data)
assert response.status_code == 200
assert response.json == -1
deftest_multiply(client, json_headers, json_data):
response = client.post("/api/multiply/", headers=json_headers, json=json_data)
assert response.status_code == 200
assert response.json == 2
deftest_divide(client, json_headers, json_data):
response = client.post("/api/divide/", headers=json_headers, json=json_data)
assert response.status_code == 200
assert response.json == 0.5
deftest_divide_by_zero(client, json_headers):
response = client.post("/api/divide/", headers=json_headers, json={"a": 1, "b": 0})
assert response.status_code == 400
assert response.json == "除数不能为零"
在这个测试中,我们定义了两个Fixtures:
Flask客户端。
API请求的JSON头部信息。
如果你不熟悉API和Flask,我建议你阅读一些基础知识以便更好地理解。
上面的Fixtures允许我们定义一次客户端和JSON头部信息,并在测试中进行POST请求时复用它们。
通过conftest在多个测试中使用Fixtures 一种更高效的方法是将通用的Fixtures放在一个名为conftest.py的文件中,这样所有的单元测试文件都会自动获取到这些Fixtures。
如果你不熟悉conftest,这篇关于Pytest conftest的文章会给你一个坚实的基础。
go
import pytest
from calculator.core import Calculator
from app.app import app
@pytest.fixture(scope="module")
defcalculator():
return Calculator(2, 3)
@pytest.fixture
defcustom_calculator(scope="module"):
def_calculator(a, b):
return Calculator(a, b)
return _calculator
@pytest.fixture(scope="module")
defclient():
with app.test_client() as client:
yield client
@pytest.fixture(scope="module")
defjson_headers():
return {"Content-Type": "application/json"}
@pytest.fixture(scope="module")
defjson_data():
return {"a": 1, "b": 2}
在这里,我们定义了Fixtures,并且可以在单元测试中轻松使用它们。
我们有各种Fixtures:
Calculator Fixtures。 自定义CalculatorFixtures(参数化Fixtures)。 Flask客户端Fixtures。 JSON头部信息Fixtures。 JSON数据Fixtures。
参数化Fixtures
这些Fixtures可以接受一个或多个参数,并在运行时进行初始化。
在前面代码块的示例中,我们定义了custom_calculatorFixtures,它允许我们在测试中传递不同的(a, b)值。
你可以通过在一个Fixtures中定义另一个Fixtures来定义参数化Fixtures。
例如:
go
@pytest.fixture
def custom_calculator(scope="module"):
def _calculator(a, b):
return Calculator(a, b)
return _calculator
这个强大的功能允许我们为每个测试使用自定义值来初始化Calculator类,非常方便。
Fixtures依赖注入
Fixtures也可以被其他Fixtures调用(或请求),这被称为依赖注入。
下面的代码示例展示了这一点:
go
import pytest
classMyObject:
def__init__(self, value):
self.value = value
@pytest.fixture
defmy_object():
return MyObject("Hello, World!")
deftest_my_object(my_object):
assert my_object.value == "Hello, World!"
@pytest.fixture
defmy_dependent_object(my_object):
return MyObject(my_object.value + " Again!")
deftest_my_dependent_object(my_dependent_object):
assert my_dependent_object.value == "Hello, World! Again!"
在这里你可以看到,my_dependent_objectFixtures使用了my_objectFixtures。
除非有必要,我建议避免使用有依赖关系的Fixtures,因为这会增加复杂性,并且将Fixtures层层嵌套会使未来的重构变得困难。
自动使用Fixtures
如果你想找到一个简单的方法来避免在每个测试中都定义Fixtures,你可以在Fixtures定义中使用autouse=True标志作为参数。
当使用autouse=True时,这个Fixtures函数将自动应用于所有测试函数,而无需在每个测试函数中显式地将其作为参数传递。
如果你只想在某些测试函数中使用该Fixtures,你可以将测试函数名作为参数指定给@pytest.fixture装饰器,而不是使用autouse=True。
Fixtures作用域
Fixtures作用域定义了Fixtures的生命周期和可见性。
Fixtures的作用域决定了它将被调用的次数,以及在测试会话期间它的存活时长。
Pytest中可用的Fixtures作用域有:
function(函数作用域):为每个使用该Fixtures的测试函数创建Fixtures,并在测试函数结束时销毁。这是Fixtures的默认作用域。 class(类作用域):为每个使用该Fixtures的测试类创建一次Fixtures,并在测试类结束时销毁。 module(模块作用域):为每个使用该Fixtures的模块创建一次Fixtures,并在测试会话结束时销毁。 session(会话作用域):为每个测试会话创建一次Fixtures,并在测试会话结束时销毁。
要指定Fixtures的作用域,你可以将scope参数传递给@pytest.fixture装饰器。
选择合适的Fixtures作用域取决于Fixtures的用途和使用方式。
如果创建一个Fixtures的成本很高,例如数据库连接,你可能希望使用更高的作用域,以便在多个测试中复用该连接。
另一方面,如果一个Fixtures很轻量级,并且特定于单个测试,你可以使用默认的"function"作用域。
Fixtures中yield与return的区别
你可以使用yield和return语句将Fixtures的值提供给测试函数,但它们的行为和含义有所不同。
当你在Fixtures函数中使用yield时,设置代码会在第一次yield之前执行,而清理代码会在最后一次yield之后执行。
yield的示例:
go
import pytest
@pytest.fixture
def my_fixture():
# 设置代码
yield "Fixtures值"
# 清理代码
当你在Fixtures函数中使用return时,设置代码会在return语句之前执行,而清理代码会在return语句之后立即执行。
return的示例:
go
import pytest
@pytest.fixture
def my_fixture():
# 设置代码
fixture_value = "Fixtures值"
# 清理代码
return fixture_value
一般来说,当你需要为每个测试函数设置和清理一些资源时,通常会使用yield;而当你只需要为测试函数提供一个简单的值时,则使用return。
何时应该使用Fixtures
一般来说,Fixtures的一个很好的用例是:
客户端------数据库客户端、AWS或其他云客户端、需要设置/清理的API客户端。 测试数据------JSON或其他格式的测试数据可以很容易地导入并在多个测试中共享。 函数------一些常用的函数可以用作Fixtures。
结论
在本文中,你了解了Pytest Fixtures的优点,以及它们如何使编写和维护测试变得更加容易。
你还学习了Pytest Fixtures的基础知识以及如何定义它们。
你构建了一个由Flask API驱动的基本计算器应用程序,并使用conftest.py和在测试内部定义了Fixtures。
最后,你了解了自动使用、作用域以及如何对Fixtures进行参数化,这些都是Pytest非常强大的功能。
Fixtures可用于设置数据库连接、加载测试数据、初始化复杂对象,或执行测试所需的任何其他设置或清理操作。
通过使用Fixtures,你可以编写干净、模块化且可维护的测试代码,这些代码易于阅读和理解。
通过一些练习和实践,你可以利用Pytest Fixtures使你的测试过程更快、更高效、更有效。