测试 Flask 应用程序 — Flask 文档

来自菜鸟教程
Flask/docs/2.0.x/testing
跳转至:导航、​搜索

测试 Flask 应用程序

未经测试的东西已损坏。


这句话的出处未知,虽然不完全正确,但也离真相不远。 未经测试的应用程序很难改进现有代码,未经测试的应用程序的开发人员往往变得非常偏执。 如果应用程序具有自动化测试,您可以安全地进行更改并立即知道是否有任何问题。

Flask 通过公开 Werkzeug 测试 Client 并为您处理本地上下文,提供了一种测试应用程序的方法。 然后,您可以将其与您最喜欢的测试解决方案一起使用。

在本文档中,我们将使用 pytest 包作为我们测试的基础框架。 您可以使用 pip 安装它,如下所示:

$ pip install pytest

应用程序

首先,我们需要一个应用程序来测试; 我们将使用 教程 中的应用程序。 如果您还没有该应用程序,请从以下位置获取源代码 :gh:`例子 ` .

为了正确导入模块flaskr,我们需要运行tutorial文件夹中的pip install -e .


测试骨架

我们首先在应用程序根目录下添加一个测试目录。 然后创建一个 Python 文件来存储我们的测试 (test_flaskr.py)。 当我们将文件名格式化为 test_*.py 时,它会被 pytest 自动发现。

接下来,我们创建一个名为 client()pytest fixture,它配置应用程序进行测试并初始化一个新数据库:

import os
import tempfile

import pytest

from flaskr import create_app
from flaskr.db import init_db


@pytest.fixture
def client():
    db_fd, db_path = tempfile.mkstemp()
    app = create_app({'TESTING': True, 'DATABASE': db_path})

    with app.test_client() as client:
        with app.app_context():
            init_db()
        yield client

    os.close(db_fd)
    os.unlink(db_path)

每个单独的测试都会调用这个客户端装置。 它为我们提供了一个简单的应用程序接口,我们可以在其中触发对应用程序的测试请求。 客户还将为我们跟踪 cookie。

在设置过程中,TESTING 配置标志被激活。 这样做是在请求处理期间禁用错误捕获,以便在对应用程序执行测试请求时获得更好的错误报告。

因为 SQLite3 是基于文件系统的,所以我们可以很容易地使用 tempfile 模块来创建一个临时数据库并对其进行初始化。 mkstemp() 函数为我们做了两件事:它返回一个低级文件句柄和一个随机文件名,后者我们用作数据库名。 我们只需要保留 db_fd 以便我们可以使用 os.close() 函数关闭文件。

为了在测试后删除数据库,fixture 关闭文件并将其从文件系统中删除。

如果我们现在运行测试套件,我们应该看到以下输出:

$ pytest

================ test session starts ================
rootdir: ./flask/examples/flaskr, inifile: setup.cfg
collected 0 items

=========== no tests ran in 0.07 seconds ============

即使它没有运行任何实际测试,我们已经知道我们的 flaskr 应用程序在语法上是有效的,否则导入会因异常而终止。


第一次测试

现在是时候开始测试应用程序的功能了。 如果我们访问应用程序的根目录 (/),让我们检查该应用程序是否显示“到目前为止没有条目”。 为此,我们向 test_flaskr.py 添加一个新的测试函数,如下所示:

def test_empty_db(client):
    """Start with a blank database."""

    rv = client.get('/')
    assert b'No entries here so far' in rv.data

请注意,我们的测试函数以单词 test 开头; 这允许 pytest 自动将该函数识别为要运行的测试。

通过使用 client.get,我们可以向具有给定路径的应用程序发送 HTTP GET 请求。 返回值将是一个 response_class 对象。 我们现在可以使用 data 属性来检查应用程序的返回值(作为字符串)。 在这种情况下,我们确保 'No entries here so far' 是输出的一部分。

再次运行它,您应该会看到一个通过的测试:

$ pytest -v

================ test session starts ================
rootdir: ./flask/examples/flaskr, inifile: setup.cfg
collected 1 items

tests/test_flaskr.py::test_empty_db PASSED

============= 1 passed in 0.10 seconds ==============

登录和注销

我们应用程序的大部分功能仅对管理用户可用,因此我们需要一种方法来让我们的测试客户端登录和退出应用程序。 为此,我们使用所需的表单数据(用户名和密码)向登录和注销页面发出一些请求。 由于登录和注销页面重定向,我们告诉客户端 follow_redirects。

将以下两个函数添加到您的 test_flaskr.py 文件中:

def login(client, username, password):
    return client.post('/login', data=dict(
        username=username,
        password=password
    ), follow_redirects=True)


def logout(client):
    return client.get('/logout', follow_redirects=True)

现在我们可以轻松地测试登录和注销是否有效,以及它是否因无效凭据而失败。 添加这个新的测试功能:

def test_login_logout(client):
    """Make sure login and logout works."""

    username = flaskr.app.config["USERNAME"]
    password = flaskr.app.config["PASSWORD"]

    rv = login(client, username, password)
    assert b'You were logged in' in rv.data

    rv = logout(client)
    assert b'You were logged out' in rv.data

    rv = login(client, f"{username}x", password)
    assert b'Invalid username' in rv.data

    rv = login(client, username, f'{password}x')
    assert b'Invalid password' in rv.data

测试添加消息

我们还应该测试添加消息是否有效。 添加一个新的测试函数,如下所示:

def test_messages(client):
    """Test that messages work."""

    login(client, flaskr.app.config['USERNAME'], flaskr.app.config['PASSWORD'])
    rv = client.post('/add', data=dict(
        title='<Hello>',
        text='<strong>HTML</strong> allowed here'
    ), follow_redirects=True)
    assert b'No entries here so far' not in rv.data
    assert b'&lt;Hello&gt;' in rv.data
    assert b'<strong>HTML</strong> allowed here' in rv.data

在这里,我们检查文本中是否允许使用 HTML,但在标题中不允许使用 HTML,这是预期的行为。

运行它现在应该给我们三个通过的测试:

$ pytest -v

================ test session starts ================
rootdir: ./flask/examples/flaskr, inifile: setup.cfg
collected 3 items

tests/test_flaskr.py::test_empty_db PASSED
tests/test_flaskr.py::test_login_logout PASSED
tests/test_flaskr.py::test_messages PASSED

============= 3 passed in 0.23 seconds ==============

其他测试技巧

除了使用如上所示的测试客户端外,还有一个 test_request_context() 方法可以与 with 语句结合使用来临时激活请求上下文。 有了这个,您可以像在视图函数中一样访问 requestgsession 对象。 这是一个完整的示例,演示了这种方法:

from flask import Flask, request

app = Flask(__name__)

with app.test_request_context('/?name=Peter'):
    assert request.path == '/'
    assert request.args['name'] == 'Peter'

上下文绑定的所有其他对象都可以以相同的方式使用。

如果您想用不同的配置测试您的应用程序,但似乎没有好的方法可以做到这一点,请考虑切换到应用程序工厂(请参阅 应用程序工厂 )。

但是请注意,如果您使用测试请求上下文,则不会自动调用 before_request()after_request() 函数。 然而,当测试请求上下文离开 with 块时,确实会执行 teardown_request() 函数。 如果您确实希望调用 before_request() 函数,则需要自己调用 preprocess_request()

app = Flask(__name__)

with app.test_request_context('/?name=Peter'):
    app.preprocess_request()
    ...

根据您的应用程序的设计方式,这可能是打开数据库连接或类似内容所必需的。

如果你想调用 after_request() 函数,你需要调用 process_response() ,但它需要你传递一个响应对象:

app = Flask(__name__)

with app.test_request_context('/?name=Peter'):
    resp = Response('...')
    resp = app.process_response(resp)
    ...

这通常不太有用,因为此时您可以直接开始使用测试客户端。


伪造资源和上下文

0.10 版中的新功能。


一个非常常见的模式是在应用程序上下文或 flask.g 对象上存储用户授权信息和数据库连接。 对此的一般模式是在第一次使用时将对象放在那里,然后在拆卸时将其移除。 想象一下这个获取当前用户的代码:

def get_user():
    user = getattr(g, 'user', None)
    if user is None:
        user = fetch_current_user_from_database()
        g.user = user
    return user

对于测试,最好从外部覆盖此用户而无需更改某些代码。 这可以通过挂钩 flask.appcontext_pushed 信号来实现:

from contextlib import contextmanager
from flask import appcontext_pushed, g

@contextmanager
def user_set(app, user):
    def handler(sender, **kwargs):
        g.user = user
    with appcontext_pushed.connected_to(handler, app):
        yield

然后使用它:

from flask import json, jsonify

@app.route('/users/me')
def users_me():
    return jsonify(username=g.user.username)

with user_set(app, my_user):
    with app.test_client() as c:
        resp = c.get('/users/me')
        data = json.loads(resp.data)
        assert data['username'] == my_user.username

保持上下文

0.4 版中的新功能。


有时,触发常规请求但仍将上下文保留一段时间会有所帮助,以便进行额外的自省。 在 Flask 0.4 中,这可以通过将 test_client()with 块一起使用来实现:

app = Flask(__name__)

with app.test_client() as c:
    rv = c.get('/?tequila=42')
    assert request.args['tequila'] == '42'

如果您只使用 test_client() 而没有 with 块,assert 将失败并显示错误,因为 request 不再可用(因为您试图在实际请求之外使用它)。


访问和修改会话

0.8 版中的新功能。


有时,从测试客户端访问或修改会话会很有帮助。 一般有两种方法。 如果您只想确保会话将某些键设置为某些值,您可以保留上下文并访问 flask.session

with app.test_client() as c:
    rv = c.get('/')
    assert session['foo'] == 42

然而,这并不能在请求被触发之前修改会话或访问会话。 从 Flask 0.8 开始,我们提供了一个所谓的“会话事务”,它模拟适当的调用以在测试客户端的上下文中打开会话并修改它。 在事务结束时,会话被存储并准备好供测试客户端使用。 这独立于所使用的会话后端工作:

with app.test_client() as c:
    with c.session_transaction() as sess:
        sess['a_key'] = 'a value'

    # once this is reached the session was stored and ready to be used by the client
    c.get(...)

请注意,在这种情况下,您必须使用 sess 对象而不是 flask.session 代理。 然而,对象本身将提供相同的接口。


测试 JSON API

1.0 版中的新功能。


Flask 对 JSON 有很好的支持,是构建 JSON API 的流行选择。 使用 JSON 数据发出请求并检查响应中的 JSON 数据非常方便:

from flask import request, jsonify

@app.route('/api/auth')
def auth():
    json_data = request.get_json()
    email = json_data['email']
    password = json_data['password']
    return jsonify(token=generate_token(email, password))

with app.test_client() as c:
    rv = c.post('/api/auth', json={
        'email': 'flask@example.com', 'password': 'secret'
    })
    json_data = rv.get_json()
    assert verify_token(email, json_data['token'])

在测试客户端方法中传递 json 参数会将请求数据设置为 JSON 序列化对象并将内容类型设置为 application/json。 您可以使用 get_json 从请求或响应中获取 JSON 数据。


测试 CLI 命令

Click 带有 实用程序,用于测试 CLI 命令。 CliRunner 独立运行命令并捕获 Result 对象中的输出。

Flask 提供 test_cli_runner() 来创建一个 FlaskCliRunner,它自动将 Flask 应用程序传递给 CLI。 使用其 invoke() 方法以从命令行调用命令的相同方式调用命令。

import click

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

def test_hello():
    runner = app.test_cli_runner()

    # invoke the command directly
    result = runner.invoke(hello_command, ['--name', 'Flask'])
    assert 'Hello, Flask' in result.output

    # or by name
    result = runner.invoke(args=['hello'])
    assert 'World' in result.output

在上面的示例中,按名称调用命令很有用,因为它可以验证命令是否已正确注册到应用程序。

如果您想测试您的命令如何解析参数,而不运行该命令,请使用其 make_context() 方法。 这对于测试复杂的验证规则和自定义类型很有用。

def upper(ctx, param, value):
    if value is not None:
        return value.upper()

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

def test_hello_params():
    context = hello_command.make_context('hello', ['--name', 'flask'])
    assert context.params['name'] == 'FLASK'