mirror of https://github.com/pallets/flask.git
add test_cli_runner for testing app.cli commands
This commit is contained in:
parent
79f34f1769
commit
cf5525f98a
|
@ -137,6 +137,8 @@ unreleased
|
||||||
development server over HTTPS. (`#2606`_)
|
development server over HTTPS. (`#2606`_)
|
||||||
- Added :data:`SESSION_COOKIE_SAMESITE` to control the ``SameSite``
|
- Added :data:`SESSION_COOKIE_SAMESITE` to control the ``SameSite``
|
||||||
attribute on the session cookie. (`#2607`_)
|
attribute on the session cookie. (`#2607`_)
|
||||||
|
- Added :meth:`~flask.Flask.test_cli_runner` to create a Click runner
|
||||||
|
that can invoke Flask CLI commands for testing. (`#2636`_)
|
||||||
|
|
||||||
.. _pallets/meta#24: https://github.com/pallets/meta/issues/24
|
.. _pallets/meta#24: https://github.com/pallets/meta/issues/24
|
||||||
.. _#1421: https://github.com/pallets/flask/issues/1421
|
.. _#1421: https://github.com/pallets/flask/issues/1421
|
||||||
|
@ -178,6 +180,7 @@ unreleased
|
||||||
.. _#2581: https://github.com/pallets/flask/pull/2581
|
.. _#2581: https://github.com/pallets/flask/pull/2581
|
||||||
.. _#2606: https://github.com/pallets/flask/pull/2606
|
.. _#2606: https://github.com/pallets/flask/pull/2606
|
||||||
.. _#2607: https://github.com/pallets/flask/pull/2607
|
.. _#2607: https://github.com/pallets/flask/pull/2607
|
||||||
|
.. _#2636: https://github.com/pallets/flask/pull/2636
|
||||||
|
|
||||||
|
|
||||||
Version 0.12.2
|
Version 0.12.2
|
||||||
|
|
|
@ -188,6 +188,15 @@ Test Client
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
|
||||||
|
Test CLI Runner
|
||||||
|
---------------
|
||||||
|
|
||||||
|
.. currentmodule:: flask.testing
|
||||||
|
|
||||||
|
.. autoclass:: FlaskCliRunner
|
||||||
|
:members:
|
||||||
|
|
||||||
|
|
||||||
Application Globals
|
Application Globals
|
||||||
-------------------
|
-------------------
|
||||||
|
|
||||||
|
|
|
@ -413,15 +413,17 @@ with ``get_json``.
|
||||||
Testing CLI Commands
|
Testing CLI Commands
|
||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
Click comes with `utilities for testing`_ your CLI commands.
|
Click comes with `utilities for testing`_ your CLI commands. A
|
||||||
|
:class:`~click.testing.CliRunner` runs commands in isolation and
|
||||||
|
captures the output in a :class:`~click.testing.Result` object.
|
||||||
|
|
||||||
Use :meth:`CliRunner.invoke <click.testing.CliRunner.invoke>` to call
|
Flask provides :meth:`~flask.Flask.test_cli_runner` to create a
|
||||||
commands in the same way they would be called from the command line. The
|
:class:`~flask.testing.FlaskCliRunner` that passes the Flask app to the
|
||||||
:class:`~click.testing.CliRunner` runs the command in isolation and
|
CLI automatically. Use its :meth:`~flask.testing.FlaskCliRunner.invoke`
|
||||||
captures the output in a :class:`~click.testing.Result` object. ::
|
method to call commands in the same way they would be called from the
|
||||||
|
command line. ::
|
||||||
|
|
||||||
import click
|
import click
|
||||||
from click.testing import CliRunner
|
|
||||||
|
|
||||||
@app.cli.command('hello')
|
@app.cli.command('hello')
|
||||||
@click.option('--name', default='World')
|
@click.option('--name', default='World')
|
||||||
|
@ -429,14 +431,22 @@ captures the output in a :class:`~click.testing.Result` object. ::
|
||||||
click.echo(f'Hello, {name}!')
|
click.echo(f'Hello, {name}!')
|
||||||
|
|
||||||
def test_hello():
|
def test_hello():
|
||||||
runner = CliRunner()
|
runner = app.test_cli_runner()
|
||||||
|
|
||||||
|
# invoke the command directly
|
||||||
result = runner.invoke(hello_command, ['--name', 'Flask'])
|
result = runner.invoke(hello_command, ['--name', 'Flask'])
|
||||||
assert 'Hello, Flask' in result.output
|
assert 'Hello, Flask' in result.output
|
||||||
|
|
||||||
|
# or by name
|
||||||
|
result = runner.invoke(args=['hello'])
|
||||||
|
assert 'World' in result.output
|
||||||
|
|
||||||
|
In the example above, invoking the command by name is useful because it
|
||||||
|
verifies that the command was correctly registered with the app.
|
||||||
|
|
||||||
If you want to test how your command parses parameters, without running
|
If you want to test how your command parses parameters, without running
|
||||||
the command, use the command's :meth:`~click.BaseCommand.make_context`
|
the command, use its :meth:`~click.BaseCommand.make_context` method.
|
||||||
method. This is useful for testing complex validation rules and custom
|
This is useful for testing complex validation rules and custom types. ::
|
||||||
types. ::
|
|
||||||
|
|
||||||
def upper(ctx, param, value):
|
def upper(ctx, param, value):
|
||||||
if value is not None:
|
if value is not None:
|
||||||
|
|
25
flask/app.py
25
flask/app.py
|
@ -311,6 +311,14 @@ class Flask(_PackageBoundObject):
|
||||||
#: .. versionadded:: 0.7
|
#: .. versionadded:: 0.7
|
||||||
test_client_class = None
|
test_client_class = None
|
||||||
|
|
||||||
|
#: The :class:`~click.testing.CliRunner` subclass, by default
|
||||||
|
#: :class:`~flask.testing.FlaskCliRunner` that is used by
|
||||||
|
#: :meth:`test_cli_runner`. Its ``__init__`` method should take a
|
||||||
|
#: Flask app object as the first argument.
|
||||||
|
#:
|
||||||
|
#: .. versionadded:: 1.0
|
||||||
|
test_cli_runner_class = None
|
||||||
|
|
||||||
#: the session interface to use. By default an instance of
|
#: the session interface to use. By default an instance of
|
||||||
#: :class:`~flask.sessions.SecureCookieSessionInterface` is used here.
|
#: :class:`~flask.sessions.SecureCookieSessionInterface` is used here.
|
||||||
#:
|
#:
|
||||||
|
@ -983,6 +991,23 @@ class Flask(_PackageBoundObject):
|
||||||
from flask.testing import FlaskClient as cls
|
from flask.testing import FlaskClient as cls
|
||||||
return cls(self, self.response_class, use_cookies=use_cookies, **kwargs)
|
return cls(self, self.response_class, use_cookies=use_cookies, **kwargs)
|
||||||
|
|
||||||
|
def test_cli_runner(self, **kwargs):
|
||||||
|
"""Create a CLI runner for testing CLI commands.
|
||||||
|
See :ref:`testing-cli`.
|
||||||
|
|
||||||
|
Returns an instance of :attr:`test_cli_runner_class`, by default
|
||||||
|
:class:`~flask.testing.FlaskCliRunner`. The Flask app object is
|
||||||
|
passed as the first argument.
|
||||||
|
|
||||||
|
.. versionadded:: 1.0
|
||||||
|
"""
|
||||||
|
cls = self.test_cli_runner_class
|
||||||
|
|
||||||
|
if cls is None:
|
||||||
|
from flask.testing import FlaskCliRunner as cls
|
||||||
|
|
||||||
|
return cls(self, **kwargs)
|
||||||
|
|
||||||
def open_session(self, request):
|
def open_session(self, request):
|
||||||
"""Creates or opens a new session. Default implementation stores all
|
"""Creates or opens a new session. Default implementation stores all
|
||||||
session data in a signed cookie. This requires that the
|
session data in a signed cookie. This requires that the
|
||||||
|
|
|
@ -12,6 +12,9 @@
|
||||||
|
|
||||||
import werkzeug
|
import werkzeug
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
|
|
||||||
|
from click.testing import CliRunner
|
||||||
|
from flask.cli import ScriptInfo
|
||||||
from werkzeug.test import Client, EnvironBuilder
|
from werkzeug.test import Client, EnvironBuilder
|
||||||
from flask import _request_ctx_stack
|
from flask import _request_ctx_stack
|
||||||
from flask.json import dumps as json_dumps
|
from flask.json import dumps as json_dumps
|
||||||
|
@ -193,3 +196,36 @@ class FlaskClient(Client):
|
||||||
top = _request_ctx_stack.top
|
top = _request_ctx_stack.top
|
||||||
if top is not None and top.preserved:
|
if top is not None and top.preserved:
|
||||||
top.pop()
|
top.pop()
|
||||||
|
|
||||||
|
|
||||||
|
class FlaskCliRunner(CliRunner):
|
||||||
|
"""A :class:`~click.testing.CliRunner` for testing a Flask app's
|
||||||
|
CLI commands. Typically created using
|
||||||
|
:meth:`~flask.Flask.test_cli_runner`. See :ref:`testing-cli`.
|
||||||
|
"""
|
||||||
|
def __init__(self, app, **kwargs):
|
||||||
|
self.app = app
|
||||||
|
super(FlaskCliRunner, self).__init__(**kwargs)
|
||||||
|
|
||||||
|
def invoke(self, cli=None, args=None, **kwargs):
|
||||||
|
"""Invokes a CLI command in an isolated environment. See
|
||||||
|
:meth:`CliRunner.invoke <click.testing.CliRunner.invoke>` for
|
||||||
|
full method documentation. See :ref:`testing-cli` for examples.
|
||||||
|
|
||||||
|
If the ``obj`` argument is not given, passes an instance of
|
||||||
|
:class:`~flask.cli.ScriptInfo` that knows how to load the Flask
|
||||||
|
app being tested.
|
||||||
|
|
||||||
|
:param cli: Command object to invoke. Default is the app's
|
||||||
|
:attr:`~flask.app.Flask.cli` group.
|
||||||
|
:param args: List of strings to invoke the command with.
|
||||||
|
|
||||||
|
:return: a :class:`~click.testing.Result` object.
|
||||||
|
"""
|
||||||
|
if cli is None:
|
||||||
|
cli = self.app.cli
|
||||||
|
|
||||||
|
if 'obj' not in kwargs:
|
||||||
|
kwargs['obj'] = ScriptInfo(create_app=lambda: self.app)
|
||||||
|
|
||||||
|
return super(FlaskCliRunner, self).invoke(cli, args, **kwargs)
|
||||||
|
|
|
@ -8,15 +8,16 @@
|
||||||
:copyright: © 2010 by the Pallets team.
|
:copyright: © 2010 by the Pallets team.
|
||||||
:license: BSD, see LICENSE for more details.
|
:license: BSD, see LICENSE for more details.
|
||||||
"""
|
"""
|
||||||
|
import click
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
import flask
|
import flask
|
||||||
import werkzeug
|
import werkzeug
|
||||||
|
|
||||||
from flask._compat import text_type
|
from flask._compat import text_type
|
||||||
|
from flask.cli import ScriptInfo
|
||||||
from flask.json import jsonify
|
from flask.json import jsonify
|
||||||
from flask.testing import make_test_environ_builder
|
from flask.testing import make_test_environ_builder, FlaskCliRunner
|
||||||
|
|
||||||
|
|
||||||
def test_environ_defaults_from_config(app, client):
|
def test_environ_defaults_from_config(app, client):
|
||||||
|
@ -335,3 +336,47 @@ def test_nosubdomain(app, client):
|
||||||
|
|
||||||
assert 200 == response.status_code
|
assert 200 == response.status_code
|
||||||
assert b'xxx' == response.data
|
assert b'xxx' == response.data
|
||||||
|
|
||||||
|
|
||||||
|
def test_cli_runner_class(app):
|
||||||
|
runner = app.test_cli_runner()
|
||||||
|
assert isinstance(runner, FlaskCliRunner)
|
||||||
|
|
||||||
|
class SubRunner(FlaskCliRunner):
|
||||||
|
pass
|
||||||
|
|
||||||
|
app.test_cli_runner_class = SubRunner
|
||||||
|
runner = app.test_cli_runner()
|
||||||
|
assert isinstance(runner, SubRunner)
|
||||||
|
|
||||||
|
|
||||||
|
def test_cli_invoke(app):
|
||||||
|
@app.cli.command('hello')
|
||||||
|
def hello_command():
|
||||||
|
click.echo('Hello, World!')
|
||||||
|
|
||||||
|
runner = app.test_cli_runner()
|
||||||
|
# invoke with command name
|
||||||
|
result = runner.invoke(args=['hello'])
|
||||||
|
assert 'Hello' in result.output
|
||||||
|
# invoke with command object
|
||||||
|
result = runner.invoke(hello_command)
|
||||||
|
assert 'Hello' in result.output
|
||||||
|
|
||||||
|
|
||||||
|
def test_cli_custom_obj(app):
|
||||||
|
class NS(object):
|
||||||
|
called = False
|
||||||
|
|
||||||
|
def create_app():
|
||||||
|
NS.called = True
|
||||||
|
return app
|
||||||
|
|
||||||
|
@app.cli.command('hello')
|
||||||
|
def hello_command():
|
||||||
|
click.echo('Hello, World!')
|
||||||
|
|
||||||
|
script_info = ScriptInfo(create_app=create_app)
|
||||||
|
runner = app.test_cli_runner()
|
||||||
|
runner.invoke(hello_command, obj=script_info)
|
||||||
|
assert NS.called
|
||||||
|
|
Loading…
Reference in New Issue