用 pytest 测试 Flask 请求、会话与命令

原文:Testing Flask Applications;作者/维护方:Pallets / Flask 文档贡献者。中文翻译与技术整理:未完纪。核验日期:2026-10-05。

Flask 提供了测试应用各个部分的辅助工具。本指南使用 pytest,把应用、测试客户端和命令行执行器组织成可复用 fixture,再分别验证请求、会话和上下文行为。

依赖安装命令是 pip install pytest;应在项目的隔离环境中按依赖锁文件安装。原文另有 Flaskr 测试教程,介绍如何为示例博客实现 100% 覆盖率。覆盖率是代码被触达的指标,并不证明业务规则正确或系统安全。

组织测试,先找出自己负责的行为

测试通常放在 tests 目录里。模块名、测试函数名以 test_ 开头,也可以用以 Test 开头的类分组。优先测试自己编写的代码,而不是重复测试已经由依赖库维护的内部实现;把复杂业务行为提取成独立函数,会更容易构造清晰断言。

用 fixture 创建应用、客户端与命令执行器

简单的 pytest fixture 可以直接返回对象,也可以先准备资源,通过 yield 交给测试,在测试结束后清理资源。下面的 fixture 可放进 tests/conftest.py:

import pytest
from my_project import create_app

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

    # 在这里准备本测试专用的资源
    yield app
    # 在这里清理或重置这些测试资源

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

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

如果使用应用工厂,就在 fixture 中创建并配置应用。若项目已经有可导入的应用对象,也可以直接配置它,再用 fixture 管理附属资源。

编辑补充:TESTING=True 是测试配置开关,不会自动把数据库、文件系统、邮箱或外部 API 变成沙箱。尤其是工厂在创建时就初始化的扩展,应在初始化之前注入独立测试配置。建立和清理数据库时,只操作专门为该测试创建的库。

Flask测试入口关系图:fixture创建独立应用,test_client走完整请求分派并检查会话和响应,CLI与手动上下文按需测试更小范围。
测试入口越小,省略的框架流程越多,断言应与实际覆盖范围一致。(未完纪原创示意图,非截图或测试结果。)

发送请求并检查响应

测试客户端不用启动真实 HTTP 服务,就能向应用发送请求。Flask 的客户端扩展了 Werkzeug 客户端,支持 client.get()、client.post() 等常见方法。构造请求通常会用到路径、query_string、headers、data 或 json。

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

返回的 TestResponse 可以查看状态、响应头和响应体。response.data 是字节串;若要文本,Werkzeug 2.1 起提供 response.text,也可以使用 response.get_data(as_text=True)。

查询参数可写为 query_string={"key": "value"},请求头用 headers={...}。POST 或 PUT 的请求体可以通过 data 传入;若传原始字节,就直接使用这些字节,传字典则通常用于表单。

表单和上传文件

把字典交给 data 时,客户端会自动设置 multipart/form-data 或 application/x-www-form-urlencoded。以二进制读取模式打开的文件对象,会作为上传文件处理;也可以传入 (file, filename, content_type) 元组指定文件名和类型。

请求完成后,客户端会关闭这些文件对象,因此原示例无需再包一层常规的 with open()。固定测试文件适合放在 tests/resources,并通过测试文件自己的位置定位:

from pathlib import Path

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

这是请求构造示例,不等于已经覆盖文件类型、大小限制、恶意文件名或权限校验。本文没有附带或执行该项目的应用和测试资源;my_project、路由与 fixture 需要来自实际测试项目。

发送 JSON:修正原文的字典语法

通过 json 参数传对象,客户端会自动设置 application/json。当响应包含 JSON 时,response.json 给出反序列化后的对象。

原页 GraphQL 例子在字典中写了 variables={"id": 2},这不是合法 Python 字典键值语法。下例改为 "variables": {...}。另外,查询把 $id 声明为 String!,因此将 ID 改为字符串 "2",使示例类型自洽;若实际 schema 使用 ID! 或其他类型,应按服务定义调整。

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"

这两处是编辑修正,尚未在实际 GraphQL 服务中运行。业务测试还应检查 HTTP 状态和 GraphQL 的错误字段,避免把没有成功的数据路径当成正常结果。

检查重定向过程

客户端默认收到重定向后不会自动继续请求。传入 follow_redirects=True 后,它会跟随重定向直到获得非重定向响应。

response.history 是此前重定向响应组成的元组;每个响应的 request 属性记录生成它的请求。下面既检查跳转次数,也检查最终路径:

def test_logout_redirect(client):
    response = client.get("/logout", follow_redirects=True)
    assert len(response.history) == 1
    assert response.request.path == "/index"

访问会话与直接预置会话是两种测试

若要在请求完成后继续访问 session 等上下文变量,可以将客户端放进 with。应用和请求上下文会一直保留到退出代码块:

from flask import session

def test_access_session(client):
    with client:
        client.post("/auth/login", data={"username": "flask"})
        assert session["user_id"] == 1
    # 离开 with 后,不能继续依赖这次请求的 session 上下文

若需要在发请求之前访问或设置会话,可用 client.session_transaction()。退出代码块时,会话会被保存:

def test_modify_session(client):
    with client.session_transaction() as session:
        session["user_id"] = 1

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

第二个例子跳过登录流程,适合把测试聚焦到“已登录用户访问个人页面”的行为。它不能证明密码校验、登录限速、会话更新或身份认证流程有效;这些仍需完整登录请求测试。会话后端和测试专用密钥也应由应用 fixture 按实际配置提供,不应把真实生产密钥写进测试文件。

用 CLI runner 验证命令输出

test_cli_runner() 创建 FlaskCliRunner,调用命令并把结果收集到 Click 的 Result 对象。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

编辑补充:实际测试通常还应断言 result.exit_code,确保输出来自成功执行。runner 捕获输出,并不自动阻止命令内部修改文件、访问数据库或调用网络。

只激活需要的上下文

一些函数会读取 request、session 或 current_app,因此要求已有上下文。未必每次都要通过完整请求或 CLI 调用它们;可以直接建立相应上下文。

应用上下文常用于数据库扩展。原页的示例是:

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

这是原文保留的旧式 Query.get 用法,并非对新项目接口的推荐。应按实际 SQLAlchemy/扩展版本选用其支持的查询方式,并为返回对象添加业务断言;只查询而不断言不能验证预期结果。

请求上下文可以用 app.test_request_context() 创建,接收与测试客户端类似的构造参数:

def test_validate_user_edit(app):
    with app.test_request_context(
        "/user/2/edit", method="POST", data={"name": ""}
    ):
        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"

这里的 g 需从 Flask 导入,应用还必须提供对应的预处理逻辑。“1”只是原文测试令牌示意,不是安全的生产令牌方案。手动调用 preprocess_request() 时,还需按应用语义处理它可能返回的提前响应,不能仅检查 g 就认定鉴权流程已全面通过。

测试客户端不会启动真实 HTTP 服务,所以这些测试也不覆盖反向代理、TLS、浏览器 JavaScript 或真实部署网络。应根据要验证的行为选择测试层次,并明确每组断言能够证明什么。

来源、版本与使用说明

根据 Flask 3.1.x 测试指南全文翻译整理。Copyright 2010 Pallets,BSD-3-Clause;完整许可条件与免责声明保留在本文下方。JSON 字典语法、变量类型和安全/测试边界说明为编辑修改。本文只做静态审核,没有安装 Flask、pytest 或执行任何应用测试。

版权与许可全文

以下保留本页涉及的来源材料或示例代码的版权、许可条件与免责声明;各自适用范围依原声明。中文翻译及编辑标注:未完纪,2026-10-05。

LICENSE-Flask.txt

Copyright 2010 Pallets

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice,
   this list of conditions and the following disclaimer.

2. Redistributions in binary form must reproduce the above copyright
   notice, this list of conditions and the following disclaimer in the
   documentation and/or other materials provided with the distribution.

3. Neither the name of the copyright holder nor the names of its
   contributors may be used to endorse or promote products derived from
   this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容