如何在Flask中执行单元测试

简介

测试是软件开发过程中的必要环节,它确保代码按预期行为运行且没有缺陷。在Python中,pytest是一个非常流行的测试框架,它比标准的unit test模块(这是一个内置的Python测试框架,是标准库的一部分)提供了多个优势。pytest包括更简单的语法、更好的输出、强大的固定件以及丰富的插件生态系统。本教程将指导您设置Flask应用程序,集成pytest固定件,并使用pytest编写单元测试。

先决条件

在开始之前,您需要以下内容:

  • 一台运行Ubuntu的服务器和一个具有sudo权限的非root用户以及一个活动的防火墙。要了解如何设置,请从这个列表中选择您的发行版,并遵循我们的初始服务器设置指南。请确保使用受支持的Ubuntu版本
  • 熟悉Linux命令行。您可以查看这个关于Linux命令行入门的指南。

  • 对Python编程和Python中的pytest测试框架有基本了解。您可以参考我们的教程PyTest Python测试框架,了解更多关于pytest的信息。

  • 在您的Ubuntu系统中安装Python 3.7或更高版本。要了解如何在Ubuntu上运行Python脚本,可以参考我们的教程

    如何在Ubuntu上运行Python脚本

为什么pytest是比unittest更好的选择

pytest相较于内置的unittest框架有几个优点:

  • Pytest允许你用更少的样板代码编写测试,使用简单的断言语句而不是unittest需要的更冗长的方法。

  • 它提供了更详细和可读的输出,使识别测试失败的原因和位置变得更加容易。

  • Pytest 的 fixtures 允许更灵活和可重用的测试设置,比 unittest 的 setUptearDown 方法更为灵活。

  • 它使得用多个输入集运行同一测试函数变得容易,而在 unittest 中这一点并不那么直观。

  • Pytest 拥有丰富的插件集合,扩展了它的功能,从代码覆盖工具到并行测试执行。

  • 它会自动发现符合命名约定的测试文件和函数,节省了管理测试套件的时间和精力。

鉴于这些优势,pytest 通常是被现代 Python 测试的首选工具。现在我们来设置一个 Flask 应用程序,并使用 pytest 编写单元测试。

步骤1 – 设置环境

Ubuntu 24.04 默认带有 Python 3。打开终端,运行以下命令以确保 Python 3 已安装:

root@ubuntu:~# python3 --version
Python 3.12.3

如果您的计算机上已经安装了 Python 3,上述命令将返回 Python 3 的当前版本。如果不安装,您可以运行以下命令来安装 Python 3:

root@ubuntu:~# sudo apt install python3

接下来,您需要在系统上安装 pip 包安装器:

root@ubuntu:~# sudo apt install python3-pip

一旦安装了 pip,让我们安装 Flask。

步骤2 – 创建 Flask 应用程序

首先,让我们创建一个简单的 Flask 应用程序。为您的项目创建一个新的目录,并进入该目录:

root@ubuntu:~# mkdir flask_testing_app
root@ubuntu:~# cd flask_testing_app

现在,让我们创建并激活一个虚拟环境来管理依赖关系:

root@ubuntu:~# python3 -m venv venv
root@ubuntu:~# source venv/bin/activate

使用pip安装Flask:

root@ubuntu:~# pip install Flask

现在,让我们创建一个简单的Flask应用程序。创建一个名为app.py的新文件,并添加以下代码:

app.py
from flask import Flask, jsonify

app = Flask(__name__)

@app.route('/')
def home():
    return jsonify(message="Hello, Flask!")

@app.route('/about')
def about():
    return jsonify(message="This is the About page")

@app.route('/multiply/<int:x>/<int:y>')
def multiply(x, y):
    result = x * y
    return jsonify(result=result)

if __name__ == '__main__':
    app.run(debug=True)

此应用程序有三个路由:

  • /:返回一个简单的“你好,Flask!”消息。
  • /about:返回一个简单的“这是关于页面”消息。
  • /multiply/<int:x>/<int:y>:将两个整数相乘并返回结果。

要运行应用程序,请执行以下命令:

root@ubuntu:~# flask run
output
* Serving Flask app "app" (lazy loading) * Environment: production WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. * Debug mode: on * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

从上述输出中,您可以注意到服务器正在http://127.0.0.1上运行,并监听端口5000。打开另一个Ubuntu控制台,并依次执行以下curl命令:

  • GET: curl http://127.0.0.1:5000/
  • GET: curl http://127.0.0.1:5000/about
  • GET: curl http://127.0.0.1:5000/multiply/10/20

让我们来了解一下这些GET请求的作用:

  1. curl http://127.0.0.1:5000/
    这个命令向我们的Flask应用的根路由(“/”)发送一个GET请求。服务器回应了一个包含消息“Hello, Flask!”的JSON对象,展示了我们主页路由的基本功能。

  2. curl http://127.0.0.1:5000/about
    这个命令向/about路由发送一个GET请求。服务器回应了一个包含消息“This is the About page”的JSON对象。这表明我们的路由正常工作。

  3. curl http://127.0.0.1:5000/multiply/10/20
    这个命令向/multiply路由发送一个带有两个参数(10和20)的GET请求。服务器将这些数字相乘,并回应一个包含结果(200)的JSON对象。这证明了我们的乘法路由能够正确处理URL参数并进行计算。

这些GET请求允许我们与Flask应用程序的API端点交互,获取信息或在服务器上触发操作,而无需修改任何数据。它们对于获取数据、测试端点功能以及验证路由是否如预期响应非常有用。

让我们看看这些GET请求的实际操作:

root@ubuntu:~# curl http://127.0.0.1:5000/
Output
{"message":"Hello, Flask!"}
root@ubuntu:~# curl http://127.0.0.1:5000/about
Output
{"message":"This is the About page"}
root@ubuntu:~# curl http://127.0.0.1:5000/multiply/10/20
Output
{"result":200}

步骤3 – 安装pytest并编写您的第一个测试

现在您已经有了一个基本的Flask应用程序,让我们安装pytest并编写一些单元测试。

使用pip安装pytest

root@ubuntu:~# pip install pytest

创建一个tests目录来存储您的测试文件:

root@ubuntu:~# mkdir tests

现在,让我们创建一个名为test_app.py的新文件,并添加以下代码:

test_app.py
# 导入sys模块以修改Python的运行时环境
import sys
# 导入os模块以与操作系统交互
import os

# 将父目录添加到sys.path
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

# 从主app文件中导入Flask app实例
from app import app 
# 导入pytest以编写和运行测试
import pytest

@pytest.fixture
def client():
    """A test client for the app."""
    with app.test_client() as client:
        yield client

def test_home(client):
    """Test the home route."""
    response = client.get('/')
    assert response.status_code == 200
    assert response.json == {"message": "Hello, Flask!"}

def test_about(client):
    """Test the about route."""
    response = client.get('/about')
    assert response.status_code == 200
    assert response.json == {"message": "This is the About page"}

def test_multiply(client):
    """Test the multiply route with valid input."""
    response = client.get('/multiply/3/4')
    assert response.status_code == 200
    assert response.json == {"result": 12}

def test_multiply_invalid_input(client):
    """Test the multiply route with invalid input."""
    response = client.get('/multiply/three/four')
    assert response.status_code == 404

def test_non_existent_route(client):
    """Test for a non-existent route."""
    response = client.get('/non-existent')
    assert response.status_code == 404

让我们来分解这个测试文件中的功能:

  1. @pytest.fixture def client():
    这是一个pytest固定器,为我们的Flask应用创建一个测试客户端。它使用app.test_client()方法创建一个可以向我们的应用发送请求的客户端,而无需运行实际服务器。yield语句允许在测试中使用客户端,并在每个测试后正确关闭。

  2. def test_home(client):

    这个函数测试我们应用的首页路由(/)。它使用测试客户端向该路由发送GET请求,然后断言响应状态码为200(OK),并且JSON响应与预期消息匹配。

  3. def test_about(client):

    test_home类似,这个函数测试关于路由(/about)。它检查状态码是否为200,并验证JSON响应内容。

  4. def test_multiply(client):

    这个函数测试有效的乘法输入路由(/multiply/3/4)。它检查状态码是否为200,并且JSON响应包含乘法结果的正确值。

  5. def test_multiply_invalid_input(client):
    这个函数测试了 multiply 路由在接收无效输入(multiply/three/four)时的行为。它检查状态码是否为 404(未找到),这是当路由无法将字符串输入匹配到所需的整数参数时的预期行为。

  6. def test_non_existent_route(client):
    这个函数测试了当访问一个不存在的路由时应用程序的行为。它向 /non-existent 发送一个 GET 请求,该路由在我们的 Flask 应用程序中未定义。测试断言响应状态码为 404(未找到),以确保我们的应用程序能够正确处理对未定义路由的请求。

这些测试涵盖了我们 Flask 应用程序的基本功能,确保每个路由对有效输入做出正确响应,并且 multiply 路由能够正确处理无效输入。通过使用 pytest,我们可以轻松运行这些测试,以验证我们的应用程序是否按预期工作。

步骤 4 – 运行测试

要运行测试,请执行以下命令:

root@ubuntu:~# pytest

默认情况下,pytest 发现过程将递归扫描当前文件夹及其子文件夹中以 “test_” 开头或以 “_test” 结尾的文件。然后执行这些文件中找到的测试。您应该会看到类似于:

Output
platform linux -- Python 3.12.3, pytest-8.3.2, pluggy-1.5.0 rootdir: /home/user/flask_testing_app collected 5 items tests/test_app.py .... [100%] ======================================================= 5 passed in 0.19s ========================================================

这表明所有测试都已成功通过。

步骤 5: 在 pytest 中使用固定值

固定值是用于向测试提供数据或资源的函数。它们可用于设置和拆除测试环境,加载数据或执行其他设置任务。在 pytest 中,固定值是通过使用 @pytest.fixture 装饰器来定义的。

以下是如何增强现有固定值的示例。更新客户端固定值以使用设置和拆除逻辑:

test_app.py
@pytest.fixture
def client():
    """Set up a test client for the app with setup and teardown logic."""
    print("\nSetting up the test client")
    with app.test_client() as client:
        yield client  # 测试在这里发生
    print("Tearing down the test client")

def test_home(client):
    """Test the home route."""
    response = client.get('/')
    assert response.status_code == 200
    assert response.json == {"message": "Hello, Flask!"}

def test_about(client):
    """Test the about route."""
    response = client.get('/about')
    assert response.status_code == 200
    assert response.json == {"message": "This is the About page"}

def test_multiply(client):
    """Test the multiply route with valid input."""
    response = client.get('/multiply/3/4')
    assert response.status_code == 200
    assert response.json == {"result": 12}

def test_multiply_invalid_input(client):
    """Test the multiply route with invalid input."""
    response = client.get('/multiply/three/four')
    assert response.status_code == 404

def test_non_existent_route(client):
    """Test for a non-existent route."""
    response = client.get('/non-existent')
    assert response.status_code == 404

此设置在测试输出中添加了打印语句,以展示设置和拆除阶段。如有需要,这些可以被实际的资源管理代码替换。

让我们再次尝试运行测试:

root@ubuntu:~# pytest -vs

`-v`标志增加了详细程度,而`-s`标志允许打印语句在控制台输出中显示。

你应该看到以下输出:

Output
platform linux -- Python 3.12.3, pytest-8.3.2, pluggy-1.5.0 rootdir: /home/user/flask_testing_app cachedir: .pytest_cache collected 5 items tests/test_app.py::test_home Setting up the test client PASSED Tearing down the test client tests/test_app.py::test_about Setting up the test client PASSED Tearing down the test client tests/test_app.py::test_multiply Setting up the test client PASSED Tearing down the test client tests/test_app.py::test_multiply_invalid_input Setting up the test client PASSED Tearing down the test client tests/test_app.py::test_non_existent_route Setting up the test client PASSED Tearing down the test client ============================================ 5 passed in 0.35s =============================================

步骤6:添加一个失败测试用例

让我们在现有的测试文件中添加一个失败测试用例。修改`test_app.py`文件,在文件末尾添加以下函数,以实现一个结果错误的失败测试用例:

test_app.py
def test_multiply_edge_cases(client):
    """Test the multiply route with edge cases to demonstrate failing tests."""
    # Test with zero
    response = client.get('/multiply/0/5')
    assert response.status_code == 200
    assert response.json == {"result": 0}

    # Test with large numbers (this might fail if not handled properly)
    response = client.get('/multiply/1000000/1000000')
    assert response.status_code == 200
    assert response.json == {"result": 1000000000000}

    # 故意失败的测试:结果错误
    response = client.get('/multiply/2/3')
    assert response.status_code == 200
    assert response.json == {"result": 7}, "This test should fail to demonstrate a failing case"

让我们来分解`test_multiply_edge_cases`函数并解释每个部分的作用:

  1. 零的测试:
    这个测试检查乘法函数是否正确处理零的乘法。当任何数乘以零时,我们期望结果为0。这是一个重要的边界情况,需要进行测试,因为有些实现可能会在零的乘法上有问题。

  2. 大数测试:
    此测试验证乘法函数是否能够处理大数而不溢出或精度问题。我们将两个一百万的值相乘,期望结果为一百亿。这个测试至关重要,因为它检查了函数的上限能力。请注意,如果服务器的实现不当处理大数,这可能表明需要大数库或不同的数据类型。

  3. 故意失败的测试:
    这个测试被故意设置为失败。它检查2 * 3是否等于7,这是错误的。这个测试旨在展示失败的测试在测试输出中看起来如何。这有助于理解如何识别和调试失败的测试,这是测试驱动开发和调试过程中的一项基本技能。

通过包含这些边缘案例和故意失败,您不仅测试了您的乘法路由的基本功能,还测试了它在极端条件下的行为和错误报告能力。这种测试方法有助于确保我们应用程序的健壮性和可靠性。

让我们再试一次运行测试:

root@ubuntu:~# pytest -vs

您应该看到以下输出:

Output
platform linux -- Python 3.12.3, pytest-8.3.2, pluggy-1.5.0 rootdir: /home/user/flask_testing_app cachedir: .pytest_cache collected 6 items tests/test_app.py::test_home Setting up the test client PASSED Tearing down the test client tests/test_app.py::test_about Setting up the test client PASSED Tearing down the test client tests/test_app.py::test_multiply Setting up the test client PASSED Tearing down the test client tests/test_app.py::test_multiply_invalid_input Setting up the test client PASSED Tearing down the test client tests/test_app.py::test_non_existent_route Setting up the test client PASSED Tearing down the test client tests/test_app.py::test_multiply_edge_cases Setting up the test client FAILED Tearing down the test client ================================================================= FAILURES ================================================================== _________________________________________________________ test_multiply_edge_cases __________________________________________________________ client = <FlaskClient <Flask 'app'>> def test_multiply_edge_cases(client): """Test the multiply route with edge cases to demonstrate failing tests.""" # 用零进行测试 response = client.get('/multiply/0/5') assert response.status_code == 200 assert response.json == {"result": 0} # 用大数字进行测试(如果处理不当,这可能会失败) response = client.get('/multiply/1000000/1000000') assert response.status_code == 200 assert response.json == {"result": 1000000000000} # 故意失败的测试:结果错误 response = client.get('/multiply/2/3') assert response.status_code == 200 > assert response.json == {"result": 7}, "This test should fail to demonstrate a failing case" E AssertionError: This test should fail to demonstrate a failing case E assert {'result': 6} == {'result': 7} E E Differing items: E {'result': 6} != {'result': 7} E E Full diff: E { E - 'result': 7,... E E ...Full output truncated (4 lines hidden), use '-vv' to show tests/test_app.py:61: AssertionError ========================================================== short test summary info ========================================================== FAILED tests/test_app.py::test_multiply_edge_cases - AssertionError: This test should fail to demonstrate a failing case ======================================================== 1 failed, 5 passed in 0.32s ========================================================

上述失败消息表明,在tests/test_app.py文件中的test_multiply_edge_cases测试失败了。具体来说,这个测试函数中的最后一个断言导致了失败。

这个故意的失败很有用,它展示了测试失败是如何报告的,以及失败消息中提供了哪些信息。它显示了失败发生的确切行,期望值和实际值,以及两者之间的差异。

在实际场景中,你会修复代码使测试通过,或者在预期结果不正确时调整测试。然而,在本案例中,失败是为了教育目的而故意设置的。

结论

在本教程中,我们介绍了如何使用pytest为 Flask 应用程序设置单元测试,集成pytest fixtures,并展示了测试失败时的情况。通过遵循这些步骤,你可以确保 Flask 应用程序的可靠性和可维护性,减少错误并提高代码质量。

你可以参考FlaskPytest的官方文档以了解更多信息。

Source:
https://www.digitalocean.com/community/tutorials/unit-test-in-flask