测试Flask程序

Flask 提供了一些用于测试应用程序的工具和功能。本文档介绍了用于在测试应用程序不同部分的技术。

我们将使用 pytest 框架来设置和运行我们的测试。

$ pip install pytest

这个 教程 介绍了如何编写测试来以 100% 覆盖率测试 Flaskr 博客示例应用。有关特定应用测试的详细说明,请参阅 测试教程

识别测试

测试通常位于 tests 文件夹中。测试是位于以 test_ 开头的 Python 模块中以 test_ 开头的函数。测试也可以进一步分组到以 Test 开头的类中。

确定要测试什么可能很困难。通常是尝试测试您编写的代码,而不是您使用的库的代码,因为它们已经经过测试。尝试将复杂的行为提取为单独的函数进行单独测试。

使用 Fixtures 测试

Pytest 的 fixtures 允许编写可在测试之间复用的代码片段。一个简单的 Fixture 会返回一个值,但 Fixture 也可以执行设置、产生一个值,然后执行teardown。用于应用程序、测试客户端和 CLI 运行器的 Fixture 如下所示,它们可以放在 tests/conftest.py 中。

如果您使用的是 application factory ,定义一个 app fixture来创建和配置应用实例。您可以在 yield 前后添加代码来设置和销毁其他资源,例如创建和清除数据库。

如果您不使用application factory ,而是有一个可以直接导入和配置的 app 对象。您仍然可以使用 app 装置来设置和移除资源。

import pytest
from my_project import create_app

@pytest.fixture()
def app():
    app = create_app()
    app.config.update({
        "TESTING": True,
    })

    # other setup can go here

    yield app

    # clean up / reset resources here


@pytest.fixture()
def client(app):
    return app.test_client()


@pytest.fixture()
def runner(app):
    return app.test_cli_runner()

使用测试用用户端发送请求

测试客户端无需运行实际服务器即可向应用程序发出请求。Flask 的用户端扩展了 Werkzeug的 ,请参阅该文档了解更多信息。

client 具有与常见 HTTP 请求方法匹配的方法,例如 client.get()client.post() 。它们接收许多参数来构建请求;完整文档可在 EnvironBuilder 中找到。较常使用的参数有 pathquery_stringheaders 以及 datajson 等。

要发出请求,请调用请求应使用的对应方法,并传入要测试的路由路径。一个 TestResponse 将被返回来检查响应数据。它具有响应对象的所有属性。您通常会查看 response.data ,它是视图返回的字节数据。如果您想使用文本,Werkzeug 2.1 提供了 response.text ,或者使用 response.get_data(as_text=True)

def test_request_example(client):
    response = client.get("/posts")
    assert b"<h2>Hello, World!</h2>" in response.data

传递一个字典 query_string={"key": "value", ...} 来设置查询字符串中的参数(在 URL 中的 ? 之后)。传递一个字典 headers={} 来设置请求标头。

要在 POST 或 PUT 请求中发送请求主体,请将数据传递给 data 。如果传递的是原始的bytes,则使用它作为主体。通常会传递一个字典来设置表单数据。

表单数据

要发送表单数据,请将一个字典传递给 dataContent-Type 标头将自动设置为 multipart/form-dataapplication/x-www-form-urlencoded

如果值是一个已打开并用于读取字节的文件对象( "rb" 模式),它将被视为已上传的文件。要更改检测到的文件名和内容类型,请传递一个 (file, filename, content_type) 元组。文件对象将在发出请求后关闭,因此无需使用平时的 with open() as f: 模式。

将文件存储在 tests/resources 文件夹中,然后使用 pathlib.Path 获取相对于当前测试文件的文件会很有用。

from pathlib import Path

# get the resources folder in the tests folder
resources = Path(__file__).parent / "resources"

def test_edit_user(client):
    response = client.post("/user/2/edit", data={
        "name": "Flask",
        "theme": "dark",
        "picture": (resources / "picture.png").open("rb"),
    })
    assert response.status_code == 200

JSON 数据

要发送 JSON 数据,将对象传递给 jsonContent-Type 标头将自动设置为 application/json

类似地,如果响应包含 JSON 数据,则 response.json 属性将包含反序列化的对象。

def test_json_data(client):
    response = client.post("/graphql", json={
        "query": """
            query User($id: String!) {
                user(id: $id) {
                    name
                    theme
                    picture_url
                }
            }
        """,
        variables={"id": 2},
    })
    assert response.json["data"]["user"]["name"] == "Flask"

跟随重定向

默认情况下,如果响应是一个重定向,客户端不会发出其他请求。通过将 follow_redirects=True 传递给请求方法,客户端将继续发出请求,直到收到非重定向的响应。

TestResponse.history 是一个包含所有重定向响应的元组,这些响应最终形成了最终的响应。每个响应都有一个 request 属性,用于记录产生该响应的请求。

def test_logout_redirect(client):
    response = client.get("/logout", follow_redirects=True)
    # Check that there was one redirect response.
    assert len(response.history) == 1
    # Check that the second request was to the index page.
    assert response.request.path == "/index"

访问和修改Session

要访问 Flask 的上下文变量(主要是 session ),在客户端中使用 with 语句。应用程序和请求上下文将在发出请求 保持活动状态,直到 with 块结束。

from flask import session

def test_access_session(client):
    with client:
        client.post("/auth/login", data={"username": "flask"})
        # session is still accessible
        assert session["user_id"] == 1

    # session is no longer accessible

如果您想在发出请求 之前 访问或设置会话中的值,请在 with 语句中使用客户端的 session_transaction() 方法。该方法返回一个会话对象,并在代码块结束后保存会话。

from flask import session

def test_modify_session(client):
    with client.session_transaction() as session:
        # set a user id without going through the login route
        session["user_id"] = 1

    # session is saved now

    response = client.get("/users/me")
    assert response.json["username"] == "flask"

使用 CLI Runner 运行命令

Flask 提供了 test_cli_runner() 来创建 FlaskCliRunner,它可以独立运行 CLI 命令,并将输出捕获到 Result 对象中。Flask 的运行器扩展了 Click 的运行器 ,请参阅该文档了解更多信息。

使用运行器的 invoke() 方法以与从命令行使用 flask 命令调用相同的方式调用命令。

import click

@app.cli.command("hello")
@click.option("--name", default="World")
def hello_command(name):
    click.echo(f"Hello, {name}!")

def test_hello_command(runner):
    result = runner.invoke(args="hello")
    assert "World" in result.output

    result = runner.invoke(args=["hello", "--name", "Flask"])
    assert "Flask" in result.output

依赖于上下文存在的测试

您可能有一些被视图或命令调用的函数,由于它们访问 request, session, 或 current_app ,因此需要活动的 应用程序上下文请求上下文 。与其通过发出请求或调用命令来测试它们,你可以直接创建并激活上下文。

使用 with app.app_context() 来推送应用程序上下文。例如,数据库扩展通常需要活动的应用程序上下文才能进行查询。

def test_db_post_model(app):
    with app.app_context():
        post = db.session.query(Post).get(1)

使用 with app.test_request_context() 来推送请求上下文。它接收与测试客户端的请求方法相同的参数。

def test_validate_user_edit(app):
    with app.test_request_context(
        "/user/2/edit", method="POST", data={"name": ""}
    ):
        # call a function that accesses `request`
        messages = validate_edit_user()

    assert messages["name"][0] == "Name cannot be empty."

创建测试请求上下文不会运行任何 Flask 调度代码,因此不会调用 before_request 函数。如果需要调用这些函数,通常最好发起一个完整的请求。当然,也可以手动调用它们。

def test_auth_token(app):
    with app.test_request_context("/user/2/edit", headers={"X-Auth-Token": "1"}):
        app.preprocess_request()
        assert g.user.name == "Flask"