测试 Flask 应用¶

    Flask 提供的测试渠道是使用 Werkzeug 的 类,并为你处理本地环境。你可以结合这个渠道使用你喜欢的测试工具。

    本文使用 包作为测试的基础框架。你可以像这样使用 pip 来安装它:

    首先,我们需要一个用来测试的应用。我们将使用 教程 中的应用。如果你还没有这个应用,可以下载 。

    测试骨架¶

    首先我们在应用的根文件夹中添加一个测试文件夹。然后创建一个 Python 文件来储存测试内容( testflaskr.py )。名称类似 test*.py 的文件会被pytest 自动发现。

    接着,我们创建一个名为 client() 的 ,用来配置调试应用并初始化一个新的数据库:

    1. import os
    2. import tempfile
    3.  
    4. import pytest
    5.  
    6. from flaskr import flaskr
    7.  
    8.  
    9. @pytest.fixture
    10. def client():
    11. db_fd, flaskr.app.config['DATABASE'] = tempfile.mkstemp()
    12. flaskr.app.config['TESTING'] = True
    13. client = flaskr.app.test_client()
    14.  
    15. with flaskr.app.app_context():
    16. flaskr.init_db()
    17.  
    18. yield client
    19.  
    20. os.close(db_fd)
    21. os.unlink(flaskr.app.config['DATABASE'])

    这个客户端固件会被每个独立的测试调用。它提供了一个简单的应用接口,用于向应用发送请求,还可以为我们追踪 cookie 。

    在配置中, TESTING 配置标志是被激活的。这样在处理请求过程中,错误捕捉被关闭,以利于在测试过程得到更好的错误报告。

    因为 SQLite3 是基于文件系统的,所以我们可以方便地使用 tempfile 模块创建一个临时数据库并初始化它。 函数返回两个东西:一个低级别的文件句柄和一个随机文件名。这个文件名后面将作为我们的数据库名称。我们必须把句柄保存到 db_fd 中,以便于以后用 os.close() 函数来关闭文件。

    为了在测试后删除数据库,固件关闭并删除了文件。

    如果现在进行测试,那么会输出以下内容:

    1. $ pytest
    2.  
    3. ================ test session starts ================
    4. rootdir: ./flask/examples/flaskr, inifile: setup.cfg
    5. collected 0 items
    6.  
    7. =========== no tests ran in 0.07 seconds ============

    虽然没有运行任何实际测试,但是已经可以知道我们的 flaskr 应用没有语法错误。否则在导入时会引发异常并中断运行。

    第一个测试¶

    现在开始测试应用的功能。当我们访问应用的根 URL ( / )时应该显示“ No entries here so far ”。在 test_flaskr.py 文件中新增一个测试函数来测试这个功能:

    1. def test_empty_db(client):
    2. """Start with a blank database."""
    3.  
    4. rv = client.get('/')
    5. assert b'No entries here so far' in rv.data

    注意,我们的调试函数都是以 test 开头的。这样 pytest 就会自动识别这些是用于测试的函数并运行它们。

    通过使用 client.get ,可以向应用的指定 URL 发送 HTTP GET 请求,其返回的是一个 对象。我们可以使用data 属性来检查应用的返回值(字符串类型)。在本例中,我们检查输出是否包含 'No entries here so far'

    再次运行测试,会看到通过了一个测试:

    1. $ pytest -v
    2.  
    3. ================ test session starts ================
    4. rootdir: ./flask/examples/flaskr, inifile: setup.cfg
    5. collected 1 items
    6.  
    7. tests/test_flaskr.py::test_empty_db PASSED
    8.  
    9. ============= 1 passed in 0.10 seconds ==============

    我们应用的主要功能必须登录以后才能使用,因此必须测试应用的登录和注销。测试的方法是使用规定的数据(用户名和密码)向应用发出登录和注销的请求。因为登录和注销后会重定向到别的页面,因此必须告诉客户端使用 follow_redirects 追踪重定向。

    在 文件中添加以下两个函数:

    1. def login(client, username, password):
    2. return client.post('/login', data=dict(
    3. username=username,
    4. password=password
    5. ), follow_redirects=True)
    6.  
    7. def logout(client):
    8. return client.get('/logout', follow_redirects=True)

    现在可以方便地测试登录成功、登录失败和注销功能了。下面为新增的测试函数:

    1. def test_login_logout(client):
    2. """Make sure login and logout works."""
    3.  
    4. rv = login(client, flaskr.app.config['USERNAME'], flaskr.app.config['PASSWORD'])
    5. assert b'You were logged in' in rv.data
    6.  
    7. rv = logout(client)
    8. assert b'You were logged out' in rv.data
    9.  
    10. rv = login(client, flaskr.app.config['USERNAME'] + 'x', flaskr.app.config['PASSWORD'])
    11. assert b'Invalid username' in rv.data
    12.  
    13. rv = login(client, flaskr.app.config['USERNAME'], flaskr.app.config['PASSWORD'] + 'x')
    14. assert b'Invalid password' in rv.data

    测试添加消息¶

    我们还要测试添加消息功能。添加如下测试函数:

    运行测试,应当显示通过了三个测试:

    1. $ pytest -v
    2.  
    3. ================ test session starts ================
    4. rootdir: ./flask/examples/flaskr, inifile: setup.cfg
    5. collected 3 items
    6.  
    7. tests/test_flaskr.py::test_empty_db PASSED
    8. tests/test_flaskr.py::test_login_logout PASSED
    9. tests/test_flaskr.py::test_messages PASSED
    10.  
    11. ============= 3 passed in 0.23 seconds ==============

    其他测试技巧¶

    除了使用上述测试客户端外,还可以联合 with 语句使用 方法来临时激活一个请求环境。在这个环境中可以像在视图函数中一样操作 request 、 和 session 对象。示例:

    1. import flask
    2.  
    3. app = flask.Flask(__name__)
    4.  
    5. with app.test_request_context('/?name=Peter'):
    6. assert flask.request.path == '/'
    7. assert flask.request.args['name'] == 'Peter'

    所有其他与环境绑定的对象也可以这样使用。

    如果要使用不同的配置来测试应用,而且没有什么好的测试方法,那么可以考虑使用应用工厂(参见 )。

    注意,在测试请求环境中before_request() 和 不会被自动调用。但是当调试请求环境离开 with 块时会执行teardown_request() 函数。如果需要 函数和正常情况下一样被调用,那么需要自己调用 preprocess_request()

    1. app = flask.Flask(__name__)
    2.  
    3. with app.test_request_context('/?name=Peter'):
    4. app.preprocess_request()
    5. ...

    在这函数中可以打开数据库连接或者根据应用需要打开其他类似东西。

    如果想调用 函数,那么必须调用process_response() ,并把响应对象传递给它:

    1. app = flask.Flask(__name__)
    2.  
    3. with app.test_request_context('/?name=Peter'):
    4. resp = Response('...')
    5. resp = app.process_response(resp)
    6. ...

    这个例子中的情况基本没有用处,因为在这种情况下可以直接开始使用测试客户端。

    Changelog

    New in version 0.10.


    通常情况下,我们会把用户认证信息和数据库连接储存到应用环境或者 对象中,并在第一次使用前准备好,然后在断开时删除。假设应用中得到当前用户的代码如下:

    1. def get_user():
    2. user = getattr(g, 'user', None)
    3. if user is None:
    4. user = fetch_current_user_from_database()
    5. g.user = user
    6. return user

    在测试时可以很很方便地重载用户而不用改动代码。可以先像下面这样钩接flask.appcontext_pushed 信号:

    1. from contextlib import contextmanager
    2. from flask import appcontext_pushed, g
    3.  
    4. @contextmanager
    5. def user_set(app, user):
    6. def handler(sender, **kwargs):
    7. with appcontext_pushed.connected_to(handler, app):
    8. yield

    然后使用它:

    保持环境¶

    Changelog

    New in version 0.4.


    有时候这种情形是有用的:触发一个常规请求,但是保持环境以便于做一点额外的事情。在 Flask 0.4 之后可以在 with 语句中使用 来实现:
    1. app = flask.Flask(__name__)
    2.  
    3. with app.test_client() as c:
    4. rv = c.get('/?tequila=42')
    5. assert request.args['tequila'] == '42'

    如果你在没有 with 的情况下使用 ,那么assert 会出错失败。因为无法在请求之外访问 request

    访问和修改会话¶

    Changelog

    New in version 0.8.


    有时候在测试客户端中访问和修改会话是非常有用的。通常有两方法。如果你想测试会话中的键和值是否正确,你可以使用 :

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

    但是这个方法无法修改会话或在请求发出前访问会话。自 Flask 0.8 开始,我们提供了“会话处理”,用打开测试环境中会话和修改会话,最后保存会话。处理后的会话独立于后端实际使用的会话:

    1. with app.test_client() as c:
    2. with c.session_transaction() as sess:
    3. sess['a_key'] = 'a value'
    4.  
    5. # once this is reached the session was stored

    注意在这种情况下必须使用 sess 对象来代替 flask.session 代理。sess 对象本身可以提供相同的接口。

    Changelog

    New in version 1.0.


    Flask 对 JSON 的支持非常好,并且是一个创建 JSON API 的流行选择。使用 JSON生成请求和在响应中检查 JSON 数据非常方便:

    1. from flask import request, jsonify
    2.  
    3. @app.route('/api/auth')
    4. def auth():
    5. json_data = request.get_json()
    6. email = json_data['email']
    7. password = json_data['password']
    8. return jsonify(token=generate_token(email, password))
    9.  
    10. with app.test_client() as c:
    11. rv = c.post('/api/auth', json={
    12. 'username': 'flask', 'password': 'secret'
    13. })
    14. json_data = rv.get_json()
    15. assert verify_token(email, json_data['token'])

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

    测试 CLI 命令¶

    Click 来自于 测试工具 ,可用于测试 CLI 命令。一个 独立运行命令并通过Result 对象捕获输出。

    Flask 提供 来创建一个FlaskCliRunner ,以自动传递 Flask 应用给 CLI 。用它的 方法调用命令,与在命令行中调用一样:

    1. import click
    2.  
    3. @app.cli.command('hello')
    4. @click.option('--name', default='World')
    5. def hello_command(name)
    6. click.echo(f'Hello, {name}!')
    7.  
    8. def test_hello():
    9. runner = app.test_cli_runner()
    10.  
    11. # invoke the command directly
    12. result = runner.invoke(hello_command, ['--name', 'Flask'])
    13. assert 'Hello, Flask' in result.output
    14.  
    15. # or by name
    16. result = runner.invoke(args=['hello'])
    17. assert 'World' in result.output

    在上面的例子中,通过名称引用命令的好处是可以验证命令是否在应用中已正确注册过。

    如果要在不运行命令的情况下测试运行参数解析,可以使用其 make_context() 方法。这样有助于测试复杂验证规则和自定义类型:

    1. def upper(ctx, param, value):
    2. if value is not None:
    3. return value.upper()
    4.  
    5. @app.cli.command('hello')
    6. @click.option('--name', default='World', callback=upper)
    7. def hello_command(name)
    8. click.echo(f'Hello, {name}!')
    9.  
    10. def test_hello_params():
    11. context = hello_command.make_context('hello', ['--name', 'flask'])

    原文: