7
7
import flask
8
8
import six
9
9
from flask_webtest import TestApp
10
+ from werkzeug .datastructures import ImmutableMultiDict , MultiDict
10
11
11
12
from keg import current_app , signals
12
13
from keg .utils import app_environ_get
@@ -22,6 +23,10 @@ def _config_profile(appcls):
22
23
class ContextManager (object ):
23
24
"""
24
25
Facilitates having a single instance of an application ready for testing.
26
+
27
+ By default, this is used in ``Keg.testing_prep``.
28
+
29
+ Constructor arg is the Keg app class to manage for tests.
25
30
"""
26
31
apps = {}
27
32
@@ -31,6 +36,7 @@ def __init__(self, appcls):
31
36
self .ctx = None
32
37
33
38
def ensure_current (self , config ):
39
+ """Ensure the manager's app has an instance set as flask's ``current_app``"""
34
40
35
41
if not self .app :
36
42
self .app = self .appcls ().init (use_test_profile = True , config = config )
@@ -43,9 +49,16 @@ def ensure_current(self, config):
43
49
return self .app
44
50
45
51
def cleanup (self ):
52
+ """Pop the app context"""
46
53
self .ctx .pop ()
47
54
48
55
def is_ready (self ):
56
+ """Indicates the manager's app instance exists.
57
+
58
+ The instance should be created with ``get_for``. Only one ContextManager instance will get
59
+ created in a Python process for any given app. But, ``get_for`` may be called multiple
60
+ times. The first call to ``ensure_current`` will set up the application and bring the
61
+ manager to a ready state."""
49
62
return self .app is not None
50
63
51
64
@classmethod
@@ -63,17 +76,18 @@ def get_for(cls, appcls):
63
76
def app_config (** kwargs ):
64
77
"""
65
78
Set config values on any apps instantiated while the context manager is active.
66
- This is intended to be used with cli tests where the `current_app` in the test will be
67
- different from the `current_app` when the CLI command is invoked, making it very difficult
79
+ This is intended to be used with cli tests where the `` current_app` ` in the test will be
80
+ different from the `` current_app` ` when the CLI command is invoked, making it very difficult
68
81
to dynamically set app config variables using mock.patch.dict like we normally would.
82
+
69
83
Example::
70
84
71
- class TestCLI(CLIBase):
72
- app_cls = MyApp
73
- def test_it(self):
74
- with testing.app_config(FOO_NAME='Bar'):
75
- result = self.invoke('echo-foo-name')
76
- assert 'Bar' in result.output
85
+ class TestCLI(CLIBase):
86
+ app_cls = MyApp
87
+ def test_it(self):
88
+ with testing.app_config(FOO_NAME='Bar'):
89
+ result = self.invoke('echo-foo-name')
90
+ assert 'Bar' in result.output
77
91
"""
78
92
@signals .config_complete .connect
79
93
def set_config (app ):
@@ -82,7 +96,57 @@ def set_config(app):
82
96
yield
83
97
84
98
99
+ @contextlib .contextmanager
100
+ def inrequest (* req_args , args_modifier = None , ** req_kwargs ):
101
+ """A decorator/context manager to add the flask request context to a test function.
102
+
103
+ Allows test to assume a request context without running a full view stack. Use for
104
+ unit-testing a view instance without setting up a webtest instance for the app and
105
+ running requests.
106
+
107
+ Flask's ``request.args`` is normally immutable, but in test cases, it can be helpful to
108
+ patch in args without needing to construct the URL. But, we don't want to leave them
109
+ mutable, because potential app bugs could be masked in doing so. To modify args, pass
110
+ in a callable as ``args_modifier`` that takes the args dict to be modified in-place. Args
111
+ will only be mutable for executing the modifier, then returned to immutable for the
112
+ remainder of the scope.
113
+
114
+ Assumes that ``flask.current_app`` is pointing to the desired app.
115
+
116
+ Example::
117
+
118
+ @inrequest('/mypath?foo=bar&baz=boo')
119
+ def test_in_request_args(self):
120
+ assert flask.request.args['foo'] == 'bar'
121
+
122
+ def test_request_args_mutated(self):
123
+ def args_modifier(args_dict):
124
+ args_dict['baz'] = 'custom-value'
125
+
126
+ with inrequest('/mypath?foo=bar&baz=boo', args_modifier=args_modifier):
127
+ assert flask.request.args['foo'] == 'bar'
128
+ assert flask.request.args['baz'] == 'custom-value'
129
+ """
130
+ with flask .current_app .test_request_context (* req_args , ** req_kwargs ):
131
+ if callable (args_modifier ):
132
+ # Temporarily turn args into a mutable MultiDict to be patched. Then, we must
133
+ # be sure to turn them back immutable, or else tests may end up not catching
134
+ # bugs that attempt to modify request args improperly.
135
+ new_args = MultiDict (flask .request .args )
136
+ args_modifier (new_args )
137
+ flask .request .args = ImmutableMultiDict (new_args )
138
+
139
+ yield
140
+
141
+
85
142
def invoke_command (app_cls , * args , ** kwargs ):
143
+ """Invoke a command using a CLI runner and return the result.
144
+
145
+ Optional kwargs:
146
+ - exit_code: Default 0. Process exit code to assert.
147
+ - runner: Default ``click.testing.CliRunner()``. CLI runner instance to use for invocation.
148
+ - use_test_profile: Default True. Drive invoked app to use test profile instead of default.
149
+ """
86
150
exit_code = kwargs .pop ('exit_code' , 0 )
87
151
runner = kwargs .pop ('runner' , None ) or click .testing .CliRunner ()
88
152
use_test_profile = kwargs .pop ('use_test_profile' , True )
@@ -107,6 +171,15 @@ def invoke_command(app_cls, *args, **kwargs):
107
171
108
172
109
173
class CLIBase (object ):
174
+ """Test class base for testing Keg click commands.
175
+
176
+ Creates a CLI runner instance, and allows subclass to call ``self.invoke`` with
177
+ command args.
178
+
179
+ Class attributes:
180
+ - app_cls: Optional, will default to ``flask.current_app`` class.
181
+ - cmd_name: Optional, provides default in ``self.invoke`` for ``cmd_name`` kwarg.
182
+ """
110
183
# child classes will need to set these values in order to use the class
111
184
app_cls = None
112
185
cmd_name = None
@@ -116,6 +189,7 @@ def setup_class(cls):
116
189
cls .runner = click .testing .CliRunner ()
117
190
118
191
def invoke (self , * args , ** kwargs ):
192
+ """Run a command, perform some assertions, and return the result for testing."""
119
193
cmd_name = kwargs .pop ('cmd_name' , self .cmd_name )
120
194
if cmd_name is None :
121
195
cmd_name_args = []
0 commit comments