深度剖析:Pytest Fixtures如何重塑自动化测试的可读性与高效性

关注开源优测不迷路

大数据测试过程、策略及挑战

测试框架原理,构建成功的基石

在自动化测试工作之前,你应该知道的10条建议

在自动化测试中,重要的不是工具

在编写单元测试时,是否发现自己写了很多相同/相似代码呢?

像数据库的设置与清理、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的文章会给你一个坚实的基础。

conftest.py

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使你的测试过程更快、更高效、更有效。

相关推荐
FINE!(正在努力!)2 小时前
PyTest框架学习
学习·pytest
程序员杰哥13 小时前
接口自动化测试之pytest 运行方式及前置后置封装
自动化测试·软件测试·python·测试工具·职场和发展·测试用例·pytest
测试老哥18 小时前
Pytest+Selenium UI自动化测试实战实例
自动化测试·软件测试·python·selenium·测试工具·ui·pytest
水银嘻嘻1 天前
07 APP 自动化- appium+pytest+allure框架封装
python·appium·自动化·pytest
天才测试猿2 天前
接口自动化测试之pytest接口关联框架封装
自动化测试·软件测试·python·测试工具·职场和发展·测试用例·pytest
not coder3 天前
Pytest Fixture 详解
数据库·pytest
not coder3 天前
pytest 常见问题解答 (FAQ)
开发语言·python·pytest
程序员的世界你不懂3 天前
(1)pytest简介和环境准备
pytest
not coder3 天前
Pytest Fixture 是什么?
数据库·oracle·pytest
Tester_孙大壮3 天前
pytest中的元类思想与实战应用
pytest