commit | author | age
|
6ee49a
|
1 |
.. index:: |
CM |
2 |
single: unit testing |
|
3 |
single: integration testing |
|
4 |
single: functional testing |
|
5 |
|
|
6 |
.. _testing_chapter: |
|
7 |
|
|
8 |
Unit, Integration, and Functional Testing |
|
9 |
========================================= |
|
10 |
|
|
11 |
*Unit testing* is, not surprisingly, the act of testing a "unit" in your |
|
12 |
application. In this context, a "unit" is often a function or a method of a |
|
13 |
class instance. The unit is also referred to as a "unit under test". |
|
14 |
|
|
15 |
The goal of a single unit test is to test **only** some permutation of the |
4ade6d
|
16 |
"unit under test". If you write a unit test that aims to verify the result of |
SP |
17 |
a particular codepath through a Python function, you need only be concerned |
|
18 |
about testing the code that *lives in the function body itself*. If the |
|
19 |
function accepts a parameter that represents a complex application "domain |
|
20 |
object" (such as a resource, a database connection, or an SMTP server), the |
|
21 |
argument provided to this function during a unit test *need not be* and likely |
|
22 |
*should not be* a "real" implementation object. For example, although a |
|
23 |
particular function implementation may accept an argument that represents an |
|
24 |
SMTP server object, and the function may call a method of this object when the |
|
25 |
system is operating normally that would result in an email being sent, a unit |
|
26 |
test of this codepath of the function does *not* need to test that an email is |
|
27 |
actually sent. It just needs to make sure that the function calls the method |
|
28 |
of the object provided as an argument that *would* send an email if the |
|
29 |
argument happened to be the "real" implementation of an SMTP server object. |
6ee49a
|
30 |
|
CM |
31 |
An *integration test*, on the other hand, is a different form of testing in |
|
32 |
which the interaction between two or more "units" is explicitly tested. |
4ade6d
|
33 |
Integration tests verify that the components of your application work together. |
SP |
34 |
You *might* make sure that an email was actually sent in an integration test. |
6ee49a
|
35 |
|
CM |
36 |
A *functional test* is a form of integration test in which the application is |
4ade6d
|
37 |
run "literally". You would *have to* make sure that an email was actually sent |
SP |
38 |
in a functional test, because it tests your code end to end. |
6ee49a
|
39 |
|
4ade6d
|
40 |
It is often considered best practice to write each type of tests for any given |
SP |
41 |
codebase. Unit testing often provides the opportunity to obtain better |
6ee49a
|
42 |
"coverage": it's usually possible to supply a unit under test with arguments |
CM |
43 |
and/or an environment which causes *all* of its potential codepaths to be |
|
44 |
executed. This is usually not as easy to do with a set of integration or |
bfd8d5
|
45 |
functional tests, but integration and functional testing provides a measure of |
6ee49a
|
46 |
assurance that your "units" work together, as they will be expected to when |
CM |
47 |
your application is run in production. |
|
48 |
|
|
49 |
The suggested mechanism for unit and integration testing of a :app:`Pyramid` |
|
50 |
application is the Python :mod:`unittest` module. Although this module is |
|
51 |
named :mod:`unittest`, it is actually capable of driving both unit and |
|
52 |
integration tests. A good :mod:`unittest` tutorial is available within `Dive |
eb7297
|
53 |
Into Python <http://www.diveintopython.net/unit_testing/index.html>`_ by Mark |
6ee49a
|
54 |
Pilgrim. |
CM |
55 |
|
4ade6d
|
56 |
:app:`Pyramid` provides a number of facilities that make unit, integration, and |
SP |
57 |
functional tests easier to write. The facilities become particularly useful |
|
58 |
when your code calls into :app:`Pyramid`-related framework functions. |
6ee49a
|
59 |
|
CM |
60 |
.. index:: |
|
61 |
single: test setup |
|
62 |
single: test tear down |
|
63 |
single: unittest |
|
64 |
|
|
65 |
.. _test_setup_and_teardown: |
|
66 |
|
|
67 |
Test Set Up and Tear Down |
4ade6d
|
68 |
------------------------- |
6ee49a
|
69 |
|
CM |
70 |
:app:`Pyramid` uses a "global" (actually :term:`thread local`) data structure |
a85491
|
71 |
to hold two items: the current :term:`request` and the current |
6ee49a
|
72 |
:term:`application registry`. These data structures are available via the |
CM |
73 |
:func:`pyramid.threadlocal.get_current_request` and |
4ade6d
|
74 |
:func:`pyramid.threadlocal.get_current_registry` functions, respectively. See |
SP |
75 |
:ref:`threadlocals_chapter` for information about these functions and the data |
|
76 |
structures they return. |
6ee49a
|
77 |
|
CM |
78 |
If your code uses these ``get_current_*`` functions or calls :app:`Pyramid` |
f52d59
|
79 |
code which uses ``get_current_*`` functions, you will need to call |
CM |
80 |
:func:`pyramid.testing.setUp` in your test setup and you will need to call |
|
81 |
:func:`pyramid.testing.tearDown` in your test teardown. |
4ade6d
|
82 |
:func:`~pyramid.testing.setUp` pushes a registry onto the :term:`thread local` |
SP |
83 |
stack, which makes the ``get_current_*`` functions work. It returns a |
f52d59
|
84 |
:term:`Configurator` object which can be used to perform extra configuration |
CM |
85 |
required by the code under test. :func:`~pyramid.testing.tearDown` pops the |
|
86 |
thread local stack. |
6ee49a
|
87 |
|
4ade6d
|
88 |
Normally when a Configurator is used directly with the ``main`` block of a |
SP |
89 |
Pyramid application, it defers performing any "real work" until its ``.commit`` |
|
90 |
method is called (often implicitly by the |
|
91 |
:meth:`pyramid.config.Configurator.make_wsgi_app` method). The Configurator |
|
92 |
returned by :func:`~pyramid.testing.setUp` is an *autocommitting* Configurator, |
|
93 |
however, which performs all actions implied by methods called on it |
|
94 |
immediately. This is more convenient for unit testing purposes than needing to |
|
95 |
call :meth:`pyramid.config.Configurator.commit` in each test after adding extra |
|
96 |
configuration statements. |
6ee49a
|
97 |
|
f52d59
|
98 |
The use of the :func:`~pyramid.testing.setUp` and |
4ade6d
|
99 |
:func:`~pyramid.testing.tearDown` functions allows you to supply each unit test |
SP |
100 |
method in a test case with an environment that has an isolated registry and an |
|
101 |
isolated request for the duration of a single test. Here's an example of using |
|
102 |
this feature: |
6ee49a
|
103 |
|
CM |
104 |
.. code-block:: python |
|
105 |
:linenos: |
|
106 |
|
|
107 |
import unittest |
f52d59
|
108 |
from pyramid import testing |
6ee49a
|
109 |
|
CM |
110 |
class MyTest(unittest.TestCase): |
|
111 |
def setUp(self): |
f52d59
|
112 |
self.config = testing.setUp() |
6ee49a
|
113 |
|
CM |
114 |
def tearDown(self): |
f52d59
|
115 |
testing.tearDown() |
6ee49a
|
116 |
|
4ade6d
|
117 |
The above will make sure that :func:`~pyramid.threadlocal.get_current_registry` |
SP |
118 |
called within a test case method of ``MyTest`` will return the |
|
119 |
:term:`application registry` associated with the ``config`` Configurator |
|
120 |
instance. Each test case method attached to ``MyTest`` will use an isolated |
|
121 |
registry. |
6ee49a
|
122 |
|
f52d59
|
123 |
The :func:`~pyramid.testing.setUp` and :func:`~pyramid.testing.tearDown` |
4ade6d
|
124 |
functions accept various arguments that influence the environment of the test. |
SP |
125 |
See the :ref:`testing_module` API for information about the extra arguments |
|
126 |
supported by these functions. |
6ee49a
|
127 |
|
138706
|
128 |
If you also want to make :func:`~pyramid.threadlocal.get_current_request` |
MR |
129 |
return something other than ``None`` during the course of a single test, you |
4ade6d
|
130 |
can pass a :term:`request` object into the :func:`pyramid.testing.setUp` within |
SP |
131 |
the ``setUp`` method of your test: |
6ee49a
|
132 |
|
CM |
133 |
.. code-block:: python |
|
134 |
:linenos: |
|
135 |
|
|
136 |
import unittest |
|
137 |
from pyramid import testing |
|
138 |
|
|
139 |
class MyTest(unittest.TestCase): |
|
140 |
def setUp(self): |
|
141 |
request = testing.DummyRequest() |
f52d59
|
142 |
self.config = testing.setUp(request=request) |
6ee49a
|
143 |
|
CM |
144 |
def tearDown(self): |
f52d59
|
145 |
testing.tearDown() |
6ee49a
|
146 |
|
4ade6d
|
147 |
If you pass a :term:`request` object into :func:`pyramid.testing.setUp` within |
SP |
148 |
your test case's ``setUp``, any test method attached to the ``MyTest`` test |
|
149 |
case that directly or indirectly calls |
70acd2
|
150 |
:func:`~pyramid.threadlocal.get_current_request` will receive the request |
e849fe
|
151 |
object. Otherwise, during testing, |
4ade6d
|
152 |
:func:`~pyramid.threadlocal.get_current_request` will return ``None``. We use a |
SP |
153 |
"dummy" request implementation supplied by |
|
154 |
:class:`pyramid.testing.DummyRequest` because it's easier to construct than a |
|
155 |
"real" :app:`Pyramid` request object. |
6ee49a
|
156 |
|
fbbb20
|
157 |
Test setup using a context manager |
BS |
158 |
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
|
159 |
|
4ade6d
|
160 |
An alternative style of setting up a test configuration is to use the ``with`` |
d91e5d
|
161 |
statement and :func:`pyramid.testing.testConfig` to create a :term:`context manager`. |
4ade6d
|
162 |
The context manager will call :func:`pyramid.testing.setUp` before the code |
SP |
163 |
under test and :func:`pyramid.testing.tearDown` afterwards. |
fbbb20
|
164 |
|
BS |
165 |
This style is useful for small self-contained tests. For example: |
|
166 |
|
|
167 |
.. code-block:: python |
|
168 |
:linenos: |
|
169 |
|
|
170 |
import unittest |
|
171 |
|
|
172 |
class MyTest(unittest.TestCase): |
|
173 |
|
|
174 |
def test_my_function(self): |
|
175 |
from pyramid import testing |
|
176 |
with testing.testConfig() as config: |
|
177 |
config.add_route('bar', '/bar/{id}') |
|
178 |
my_function_which_needs_route_bar() |
|
179 |
|
6ee49a
|
180 |
What? |
CM |
181 |
~~~~~ |
|
182 |
|
|
183 |
Thread local data structures are always a bit confusing, especially when |
|
184 |
they're used by frameworks. Sorry. So here's a rule of thumb: if you don't |
|
185 |
*know* whether you're calling code that uses the |
70acd2
|
186 |
:func:`~pyramid.threadlocal.get_current_registry` or |
CM |
187 |
:func:`~pyramid.threadlocal.get_current_request` functions, or you don't care |
f52d59
|
188 |
about any of this, but you still want to write test code, just always call |
CM |
189 |
:func:`pyramid.testing.setUp` in your test's ``setUp`` method and |
|
190 |
:func:`pyramid.testing.tearDown` in your tests' ``tearDown`` method. This |
4ade6d
|
191 |
won't really hurt anything if the application you're testing does not call any |
SP |
192 |
``get_current*`` function. |
6ee49a
|
193 |
|
CM |
194 |
.. index:: |
|
195 |
single: pyramid.testing |
|
196 |
single: Configurator testing API |
|
197 |
|
|
198 |
Using the ``Configurator`` and ``pyramid.testing`` APIs in Unit Tests |
f52d59
|
199 |
--------------------------------------------------------------------- |
6ee49a
|
200 |
|
3fc77c
|
201 |
The ``Configurator`` API and the :mod:`pyramid.testing` module provide a number |
6ee49a
|
202 |
of functions which can be used during unit testing. These functions make |
CM |
203 |
:term:`configuration declaration` calls to the current :term:`application |
|
204 |
registry`, but typically register a "stub" or "dummy" feature in place of the |
|
205 |
"real" feature that the code would call if it was being run normally. |
|
206 |
|
|
207 |
For example, let's imagine you want to unit test a :app:`Pyramid` view |
|
208 |
function. |
|
209 |
|
|
210 |
.. code-block:: python |
|
211 |
:linenos: |
|
212 |
|
99edc5
|
213 |
from pyramid.httpexceptions import HTTPForbidden |
6ee49a
|
214 |
|
9904b3
|
215 |
def view_fn(request): |
3bd1fa
|
216 |
if request.has_permission('edit'): |
a7e625
|
217 |
raise HTTPForbidden |
9904b3
|
218 |
return {'greeting':'hello'} |
CM |
219 |
|
613a7e
|
220 |
.. note:: |
TL |
221 |
|
|
222 |
This code implies that you have defined a renderer imperatively in a |
4ade6d
|
223 |
relevant :class:`pyramid.config.Configurator` instance, otherwise it would |
SP |
224 |
fail when run normally. |
613a7e
|
225 |
|
9904b3
|
226 |
Without doing anything special during a unit test, the call to |
3bd1fa
|
227 |
:meth:`~pyramid.request.Request.has_permission` in this view function will |
CM |
228 |
always return a ``True`` value. When a :app:`Pyramid` application starts |
4ade6d
|
229 |
normally, it will populate an :term:`application registry` using |
3bd1fa
|
230 |
:term:`configuration declaration` calls made against a :term:`Configurator`. |
4ade6d
|
231 |
But if this application registry is not created and populated (e.g., by |
3bd1fa
|
232 |
initializing the configurator with an authorization policy), like when you |
CM |
233 |
invoke application code via a unit test, :app:`Pyramid` API functions will tend |
|
234 |
to either fail or return default results. So how do you test the branch of the |
|
235 |
code in this view function that raises |
|
236 |
:exc:`~pyramid.httpexceptions.HTTPForbidden`? |
6ee49a
|
237 |
|
CM |
238 |
The testing API provided by :app:`Pyramid` allows you to simulate various |
|
239 |
application registry registrations for use under a unit testing framework |
|
240 |
without needing to invoke the actual application configuration implied by its |
9904b3
|
241 |
``main`` function. For example, if you wanted to test the above ``view_fn`` |
6ee49a
|
242 |
(assuming it lived in the package named ``my.package``), you could write a |
CM |
243 |
:class:`unittest.TestCase` that used the testing API. |
|
244 |
|
|
245 |
.. code-block:: python |
|
246 |
:linenos: |
|
247 |
|
|
248 |
import unittest |
|
249 |
from pyramid import testing |
|
250 |
|
|
251 |
class MyTest(unittest.TestCase): |
|
252 |
def setUp(self): |
f52d59
|
253 |
self.config = testing.setUp() |
6ee49a
|
254 |
|
CM |
255 |
def tearDown(self): |
f52d59
|
256 |
testing.tearDown() |
6ee49a
|
257 |
|
9904b3
|
258 |
def test_view_fn_forbidden(self): |
99edc5
|
259 |
from pyramid.httpexceptions import HTTPForbidden |
6ee49a
|
260 |
from my.package import view_fn |
9904b3
|
261 |
self.config.testing_securitypolicy(userid='hank', |
CM |
262 |
permissive=False) |
6ee49a
|
263 |
request = testing.DummyRequest() |
9904b3
|
264 |
request.context = testing.DummyResource() |
a7e625
|
265 |
self.assertRaises(HTTPForbidden, view_fn, request) |
6ee49a
|
266 |
|
9904b3
|
267 |
def test_view_fn_allowed(self): |
6ee49a
|
268 |
from my.package import view_fn |
9904b3
|
269 |
self.config.testing_securitypolicy(userid='hank', |
CM |
270 |
permissive=True) |
6ee49a
|
271 |
request = testing.DummyRequest() |
9904b3
|
272 |
request.context = testing.DummyResource() |
6ee49a
|
273 |
response = view_fn(request) |
9904b3
|
274 |
self.assertEqual(response, {'greeting':'hello'}) |
CM |
275 |
|
6ee49a
|
276 |
In the above example, we create a ``MyTest`` test case that inherits from |
d4683c
|
277 |
:class:`unittest.TestCase`. If it's in our :app:`Pyramid` application, it will |
e976a6
|
278 |
be found when ``py.test`` is run. It has two test methods. |
6ee49a
|
279 |
|
9904b3
|
280 |
The first test method, ``test_view_fn_forbidden`` tests the ``view_fn`` when |
4ade6d
|
281 |
the authentication policy forbids the current user the ``edit`` permission. Its |
SP |
282 |
third line registers a "dummy" "non-permissive" authorization policy using the |
|
283 |
:meth:`~pyramid.config.Configurator.testing_securitypolicy` method, which is a |
|
284 |
special helper method for unit testing. |
6ee49a
|
285 |
|
3bd1fa
|
286 |
We then create a :class:`pyramid.testing.DummyRequest` object which simulates a |
CM |
287 |
WebOb request object API. A :class:`pyramid.testing.DummyRequest` is a request |
|
288 |
object that requires less setup than a "real" :app:`Pyramid` request. We call |
|
289 |
the function being tested with the manufactured request. When the function is |
|
290 |
called, :meth:`pyramid.request.Request.has_permission` will call the "dummy" |
|
291 |
authentication policy we've registered through |
d4683c
|
292 |
:meth:`~pyramid.config.Configurator.testing_securitypolicy`, which denies |
TL |
293 |
access. We check that the view function raises a |
|
294 |
:exc:`~pyramid.httpexceptions.HTTPForbidden` error. |
6ee49a
|
295 |
|
cdd8d4
|
296 |
The second test method, named ``test_view_fn_allowed``, tests the alternate |
9904b3
|
297 |
case, where the authentication policy allows access. Notice that we pass |
4ade6d
|
298 |
different values to :meth:`~pyramid.config.Configurator.testing_securitypolicy` |
SP |
299 |
to obtain this result. We assert at the end of this that the view function |
|
300 |
returns a value. |
6ee49a
|
301 |
|
f52d59
|
302 |
Note that the test calls the :func:`pyramid.testing.setUp` function in its |
CM |
303 |
``setUp`` method and the :func:`pyramid.testing.tearDown` function in its |
4ade6d
|
304 |
``tearDown`` method. We assign the result of :func:`pyramid.testing.setUp` as |
SP |
305 |
``config`` on the unittest class. This is a :term:`Configurator` object and |
|
306 |
all methods of the configurator can be called as necessary within tests. If you |
|
307 |
use any of the :class:`~pyramid.config.Configurator` APIs during testing, be |
|
308 |
sure to use this pattern in your test case's ``setUp`` and ``tearDown``; these |
|
309 |
methods make sure you're using a "fresh" :term:`application registry` per test |
|
310 |
run. |
6ee49a
|
311 |
|
4ade6d
|
312 |
See the :ref:`testing_module` chapter for the entire :app:`Pyramid`-specific |
6ee49a
|
313 |
testing API. This chapter describes APIs for registering a security policy, |
4ade6d
|
314 |
registering resources at paths, registering event listeners, registering views |
SP |
315 |
and view permissions, and classes representing "dummy" implementations of a |
|
316 |
request and a resource. |
6ee49a
|
317 |
|
2033ee
|
318 |
.. seealso:: |
SP |
319 |
|
|
320 |
See also the various methods of the :term:`Configurator` documented in |
|
321 |
:ref:`configuration_module` that begin with the ``testing_`` prefix. |
6ee49a
|
322 |
|
CM |
323 |
.. index:: |
|
324 |
single: integration tests |
|
325 |
|
|
326 |
.. _integration_tests: |
|
327 |
|
|
328 |
Creating Integration Tests |
|
329 |
-------------------------- |
|
330 |
|
|
331 |
In :app:`Pyramid`, a *unit test* typically relies on "mock" or "dummy" |
138706
|
332 |
implementations to give the code under test enough context to run. |
6ee49a
|
333 |
|
CM |
334 |
"Integration testing" implies another sort of testing. In the context of a |
138706
|
335 |
:app:`Pyramid` integration test, the test logic exercises the functionality of |
MR |
336 |
the code under test *and* its integration with the rest of the :app:`Pyramid` |
6ee49a
|
337 |
framework. |
CM |
338 |
|
138706
|
339 |
Creating an integration test for a :app:`Pyramid` application usually means |
MR |
340 |
invoking the application's ``includeme`` function via |
|
341 |
:meth:`pyramid.config.Configurator.include` within the test's setup code. This |
|
342 |
causes the entire :app:`Pyramid` environment to be set up, simulating what |
|
343 |
happens when your application is run "for real". This is a heavy-hammer way of |
|
344 |
making sure that your tests have enough context to run properly, and tests your |
|
345 |
code's integration with the rest of :app:`Pyramid`. |
6ee49a
|
346 |
|
138706
|
347 |
.. seealso:: |
6ee49a
|
348 |
|
9c94e1
|
349 |
See also :ref:`including_configuration` |
6ee49a
|
350 |
|
138706
|
351 |
Writing unit tests that use the :class:`~pyramid.config.Configurator` API to |
MR |
352 |
set up the right "mock" registrations is often preferred to creating |
|
353 |
integration tests. Unit tests will run faster (because they do less for each |
|
354 |
test) and are usually easier to reason about. |
6ee49a
|
355 |
|
CM |
356 |
.. index:: |
|
357 |
single: functional tests |
|
358 |
|
|
359 |
.. _functional_tests: |
|
360 |
|
|
361 |
Creating Functional Tests |
|
362 |
------------------------- |
|
363 |
|
|
364 |
Functional tests test your literal application. |
|
365 |
|
138706
|
366 |
In Pyramid, functional tests are typically written using the :term:`WebTest` |
MR |
367 |
package, which provides APIs for invoking HTTP(S) requests to your application. |
e976a6
|
368 |
We also like ``py.test`` and ``pytest-cov`` to provide simple testing and |
SP |
369 |
coverage reports. |
6ee49a
|
370 |
|
e976a6
|
371 |
Regardless of which testing :term:`package` you use, be sure to add a |
SP |
372 |
``tests_require`` dependency on that package to your application's ``setup.py`` |
fb77c9
|
373 |
file. Using the project ``myproject`` generated by the starter cookiecutter as |
e976a6
|
374 |
described in :doc:`project`, we would insert the following code immediately |
fb77c9
|
375 |
following the ``requires`` block in the file ``myproject/setup.py``. |
6ee49a
|
376 |
|
fb77c9
|
377 |
.. literalinclude:: myproject/setup.py |
e976a6
|
378 |
:language: python |
bb079f
|
379 |
:lines: 11-23 |
SP |
380 |
:lineno-match: |
|
381 |
:emphasize-lines: 9- |
6ee49a
|
382 |
|
312aa1
|
383 |
Remember to change the dependency. |
SP |
384 |
|
fb77c9
|
385 |
.. literalinclude:: myproject/setup.py |
e976a6
|
386 |
:language: python |
bb079f
|
387 |
:lines: 42-46 |
SP |
388 |
:lineno-match: |
e976a6
|
389 |
:emphasize-lines: 2-4 |
312aa1
|
390 |
|
e976a6
|
391 |
As always, whenever you change your dependencies, make sure to run the correct |
SP |
392 |
``pip install -e`` command. |
312aa1
|
393 |
|
SP |
394 |
.. code-block:: bash |
|
395 |
|
e976a6
|
396 |
$VENV/bin/pip install -e ".[testing]" |
312aa1
|
397 |
|
SP |
398 |
In your ``MyPackage`` project, your :term:`package` is named ``myproject`` |
|
399 |
which contains a ``views`` module, which in turn contains a :term:`view` |
|
400 |
function ``my_view`` that returns an HTML body when the root URL is invoked: |
6ee49a
|
401 |
|
fb77c9
|
402 |
.. literalinclude:: myproject/myproject/views.py |
138706
|
403 |
:linenos: |
MR |
404 |
:language: python |
6ee49a
|
405 |
|
312aa1
|
406 |
The following example functional test demonstrates invoking the above |
4ade6d
|
407 |
:term:`view`: |
138706
|
408 |
|
fb77c9
|
409 |
.. literalinclude:: myproject/myproject/tests.py |
138706
|
410 |
:linenos: |
MR |
411 |
:pyobject: FunctionalTests |
|
412 |
:language: python |
|
413 |
|
|
414 |
When this test is run, each test method creates a "real" :term:`WSGI` |
|
415 |
application using the ``main`` function in your ``myproject.__init__`` module, |
|
416 |
using :term:`WebTest` to wrap that WSGI application. It assigns the result to |
312aa1
|
417 |
``self.testapp``. In the test named ``test_root``, the ``TestApp``'s ``GET`` |
138706
|
418 |
method is used to invoke the root URL. Finally, an assertion is made that the |
312aa1
|
419 |
returned HTML contains the text ``Pyramid``. |
6ee49a
|
420 |
|
4ade6d
|
421 |
See the :term:`WebTest` documentation for further information about the methods |
SP |
422 |
available to a :class:`webtest.app.TestApp` instance. |