Browse Source

Merge pull request #2636 from pallets/test-cli-runner

add test_cli_runner for testing app.cli commands
pull/2635/head
David Lord 7 years ago committed by GitHub
parent
commit
4a7db66474
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  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`_) 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

9
docs/api.rst

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

30
docs/testing.rst

@ -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

@ -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

36
flask/testing.py

@ -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)

49
tests/test_testing.py

@ -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…
Cancel
Save