Browse Source

add test_cli_runner for testing app.cli commands

pull/2636/head
David Lord 7 years ago
parent
commit
cf5525f98a
No known key found for this signature in database
GPG Key ID: 7A1C87E3F5BC42A8
  1. 3
      CHANGES.rst
  2. 9
      docs/api.rst
  3. 30
      docs/testing.rst
  4. 25
      flask/app.py
  5. 36
      flask/testing.py
  6. 49
      tests/test_testing.py

3
CHANGES.rst

@ -137,6 +137,8 @@ unreleased
development server over HTTPS. (`#2606`_)
- Added :data:`SESSION_COOKIE_SAMESITE` to control the ``SameSite``
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
.. _#1421: https://github.com/pallets/flask/issues/1421
@ -178,6 +180,7 @@ unreleased
.. _#2581: https://github.com/pallets/flask/pull/2581
.. _#2606: https://github.com/pallets/flask/pull/2606
.. _#2607: https://github.com/pallets/flask/pull/2607
.. _#2636: https://github.com/pallets/flask/pull/2636
Version 0.12.2

9
docs/api.rst

@ -188,6 +188,15 @@ Test Client
:members:
Test CLI Runner
---------------
.. currentmodule:: flask.testing
.. autoclass:: FlaskCliRunner
:members:
Application Globals
-------------------

30
docs/testing.rst

@ -413,15 +413,17 @@ with ``get_json``.
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
commands in the same way they would be called from the command line. The
:class:`~click.testing.CliRunner` runs the command in isolation and
captures the output in a :class:`~click.testing.Result` object. ::
Flask provides :meth:`~flask.Flask.test_cli_runner` to create a
:class:`~flask.testing.FlaskCliRunner` that passes the Flask app to the
CLI automatically. Use its :meth:`~flask.testing.FlaskCliRunner.invoke`
method to call commands in the same way they would be called from the
command line. ::
import click
from click.testing import CliRunner
@app.cli.command('hello')
@click.option('--name', default='World')
@ -429,14 +431,22 @@ captures the output in a :class:`~click.testing.Result` object. ::
click.echo(f'Hello, {name}!')
def test_hello():
runner = CliRunner()
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
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
the command, use the command's :meth:`~click.BaseCommand.make_context`
method. This is useful for testing complex validation rules and custom
types. ::
the command, use its :meth:`~click.BaseCommand.make_context` method.
This is useful for testing complex validation rules and custom types. ::
def upper(ctx, param, value):
if value is not None:

25
flask/app.py

@ -311,6 +311,14 @@ class Flask(_PackageBoundObject):
#: .. versionadded:: 0.7
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
#: :class:`~flask.sessions.SecureCookieSessionInterface` is used here.
#:
@ -983,6 +991,23 @@ class Flask(_PackageBoundObject):
from flask.testing import FlaskClient as cls
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):
"""Creates or opens a new session. Default implementation stores all
session data in a signed cookie. This requires that the

36
flask/testing.py

@ -12,6 +12,9 @@
import werkzeug
from contextlib import contextmanager
from click.testing import CliRunner
from flask.cli import ScriptInfo
from werkzeug.test import Client, EnvironBuilder
from flask import _request_ctx_stack
from flask.json import dumps as json_dumps
@ -193,3 +196,36 @@ class FlaskClient(Client):
top = _request_ctx_stack.top
if top is not None and top.preserved:
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)

49
tests/test_testing.py

@ -8,15 +8,16 @@
:copyright: © 2010 by the Pallets team.
:license: BSD, see LICENSE for more details.
"""
import click
import pytest
import flask
import werkzeug
from flask._compat import text_type
from flask.cli import ScriptInfo
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):
@ -335,3 +336,47 @@ def test_nosubdomain(app, client):
assert 200 == response.status_code
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…
Cancel
Save