feat(testing): Add TestClient class and a pytest example (#862)

* feat(testing): Add TestClient class and a pytest example

Add a contextual wrapper for the simulate_* methods and a pytest
example demonstrating its use.

Also deprecate "api" in favor of "app" in TestCase, since these utils
can work with any WSGI app (not just those that express HTTP APIs). This
terminology makes TestCase more consistent with the new simulate_*
functions and with TestClient.

Finally, deprecate "api_class" in TestCase and update the docstring
for the testing module to demonstrate a less hacky alternative for
controlling the api instance type. As a bonus, this avoids having to
try and alias api_class (a class variable).

Partially implements #16

* doc(testing): Clarify 'myapp' example code
This commit is contained in:
Kurt Griffiths
2016-08-17 22:22:15 -05:00
committed by John Vrbanac
parent 2b63419867
commit fb7ca62fea
4 changed files with 205 additions and 169 deletions

View File

@@ -4,9 +4,10 @@ Testing
=======
.. automodule:: falcon.testing
:members: Result, TestCase, SimpleTestResource, StartResponseMock,
:members: Result,
simulate_request, simulate_get, simulate_head, simulate_post,
simulate_put, simulate_options, simulate_patch, simulate_delete,
TestClient, TestCase, SimpleTestResource, StartResponseMock,
capture_responder_args, rand_string, create_environ
Deprecated

View File

@@ -16,23 +16,58 @@
This package contains various test classes and utility functions to
support functional testing for both Falcon-based apps and the Falcon
framework itself::
framework itself. Both unittest-style and pytest-style tests are
supported::
# -----------------------------------------------------------------
# unittest-style
# -----------------------------------------------------------------
from falcon import testing
from myapp import app
import myapp
class TestMyApp(testing.TestCase):
class MyTestCase(testing.TestCase):
def setUp(self):
super(TestMyApp, self).setUp()
self.api = app.create_api()
super(MyTestCase, self).setUp()
def test_get_message(self):
# Assume the hypothetical `myapp` package has a
# function called `create()` to initialize and
# return a `falcon.API` instance.
self.app = myapp.create()
class TestMyApp(MyTestCase):
def test_get_message(self):
doc = {u'message': u'Hello world!'}
result = self.simulate_get('/messages/42')
self.assertEqual(result.json, doc)
# -----------------------------------------------------------------
# pytest-style
# -----------------------------------------------------------------
from falcon import testing
import pytest
import myapp
@pytest.fixture(scope='module')
def client():
# Assume the hypothetical `myapp` package has a
# function called `create()` to initialize and
# return a `falcon.API` instance.
return testing.TestClient(myapp.create())
def test_get_message(client):
doc = {u'message': u'Hello world!'}
result = self.simulate_get('/messages/42')
self.assertEqual(result.json, doc)
For additional examples, see also Falcon's own test suite.
result = client.simulate_get('/messages/42')
assert result.json == doc
"""
# Hoist classes and functions into the falcon.testing namespace

View File

@@ -125,7 +125,9 @@ def simulate_request(app, method='GET', path='/', query_string=None,
headers=None, body=None, file_wrapper=None):
"""Simulates a request to a WSGI application.
Performs a request against a WSGI application callable.
Performs a request against a WSGI application. Uses
:any:`wsgiref.validate` to ensure the response is valid
WSGI.
Keyword Args:
app (callable): The WSGI application to call
@@ -186,7 +188,9 @@ def simulate_request(app, method='GET', path='/', query_string=None,
def simulate_get(app, path, **kwargs):
"""Simulates a GET request to a WSGI application.
Equivalent to ``simulate_request(app, 'GET', ...)``
Equivalent to::
simulate_request(app, 'GET', path, **kwargs)
Args:
app (callable): The WSGI application to call
@@ -204,7 +208,9 @@ def simulate_get(app, path, **kwargs):
def simulate_head(app, path, **kwargs):
"""Simulates a HEAD request to a WSGI application.
Equivalent to ``simulate_request(app, 'HEAD', ...)``
Equivalent to::
simulate_request(app, 'HEAD', path, **kwargs)
Args:
app (callable): The WSGI application to call
@@ -222,7 +228,9 @@ def simulate_head(app, path, **kwargs):
def simulate_post(app, path, **kwargs):
"""Simulates a POST request to a WSGI application.
Equivalent to ``simulate_request(app, 'POST', ...)``
Equivalent to::
simulate_request(app, 'POST', path, **kwargs)
Args:
app (callable): The WSGI application to call
@@ -244,7 +252,9 @@ def simulate_post(app, path, **kwargs):
def simulate_put(app, path, **kwargs):
"""Simulates a PUT request to a WSGI application.
Equivalent to ``simulate_request(app, 'PUT', ...)``
Equivalent to::
simulate_request(app, 'PUT', path, **kwargs)
Args:
app (callable): The WSGI application to call
@@ -266,7 +276,9 @@ def simulate_put(app, path, **kwargs):
def simulate_options(app, path, **kwargs):
"""Simulates an OPTIONS request to a WSGI application.
Equivalent to ``simulate_request(app, 'OPTIONS', ...)``
Equivalent to::
simulate_request(app, 'OPTIONS', path, **kwargs)
Args:
app (callable): The WSGI application to call
@@ -284,7 +296,9 @@ def simulate_options(app, path, **kwargs):
def simulate_patch(app, path, **kwargs):
"""Simulates a PATCH request to a WSGI application.
Equivalent to ``simulate_request(app, 'PATCH', ...)``
Equivalent to::
simulate_request(app, 'PATCH', path, **kwargs)
Args:
app (callable): The WSGI application to call
@@ -306,7 +320,9 @@ def simulate_patch(app, path, **kwargs):
def simulate_delete(app, path, **kwargs):
"""Simulates a DELETE request to a WSGI application.
Equivalent to ``simulate_request(app, 'DELETE', ...)``
Equivalent to::
simulate_request(app, 'DELETE', path, **kwargs)
Args:
app (callable): The WSGI application to call
@@ -319,3 +335,87 @@ def simulate_delete(app, path, **kwargs):
(default: ``None``)
"""
return simulate_request(app, 'DELETE', path, **kwargs)
class TestClient(object):
""""Simulates requests to a WSGI application.
This class provides a contextual wrapper for Falcon's simulate_*
test functions. It lets you replace this::
simulate_get(app, '/messages')
simulate_head(app, '/messages')
with this::
client = TestClient(app)
client.simulate_get('/messages')
client.simulate_head('/messages')
Args:
app (callable): A WSGI application to target when simulating
requests
"""
def __init__(self, app):
self.app = app
def simulate_get(self, path='/', **kwargs):
"""Simulates a GET request to a WSGI application.
See also: :py:meth:`falcon.testing.simulate_get`.
"""
return simulate_get(self.app, path, **kwargs)
def simulate_head(self, path='/', **kwargs):
"""Simulates a HEAD request to a WSGI application.
See also: :py:meth:`falcon.testing.simulate_head`.
"""
return simulate_head(self.app, path, **kwargs)
def simulate_post(self, path='/', **kwargs):
"""Simulates a POST request to a WSGI application.
See also: :py:meth:`falcon.testing.simulate_post`.
"""
return simulate_post(self.app, path, **kwargs)
def simulate_put(self, path='/', **kwargs):
"""Simulates a PUT request to a WSGI application.
See also: :py:meth:`falcon.testing.simulate_put`.
"""
return simulate_put(self.app, path, **kwargs)
def simulate_options(self, path='/', **kwargs):
"""Simulates an OPTIONS request to a WSGI application.
See also: :py:meth:`falcon.testing.simulate_options`.
"""
return simulate_options(self.app, path, **kwargs)
def simulate_patch(self, path='/', **kwargs):
"""Simulates a PATCH request to a WSGI application.
See also: :py:meth:`falcon.testing.simulate_patch`.
"""
return simulate_patch(self.app, path, **kwargs)
def simulate_delete(self, path='/', **kwargs):
"""Simulates a DELETE request to a WSGI application.
See also: :py:meth:`falcon.testing.simulate_delete`.
"""
return simulate_delete(self.app, path, **kwargs)
def simulate_request(self, *args, **kwargs):
"""Simulates a request to a WSGI application.
Wraps :py:meth:`falcon.testing.simulate_request` to perform a
WSGI request directly against ``self.app``. Equivalent to::
falcon.testing.simulate_request(self.app, *args, **kwargs)
"""
return simulate_request(self.app, *args, **kwargs)

View File

@@ -25,11 +25,11 @@ except ImportError: # pragma: nocover
import falcon
import falcon.request
from falcon.testing import client
from falcon.testing.client import TestClient
from falcon.testing.client import Result # NOQA - hoist for backwards compat
class TestCase(unittest.TestCase):
class TestCase(unittest.TestCase, TestClient):
"""Extends :py:mod:`unittest` to support WSGI functional testing.
Note:
@@ -38,30 +38,65 @@ class TestCase(unittest.TestCase):
This base class provides some extra plumbing for unittest-style
test cases, to help simulate WSGI calls without having to spin up
an actual web server. Simply inherit from this class in your test
case classes instead of :py:class:`unittest.TestCase` or
:py:class:`testtools.TestCase`.
an actual web server. Various simulation methods are derived
from :py:class:`falcon.testing.TestClient`.
Simply inherit from this class in your test case classes instead of
:py:class:`unittest.TestCase` or :py:class:`testtools.TestCase`.
Attributes:
api_class (class): An API class or factory method to use when
instantiating the ``api`` instance (default:
:py:class:`falcon.API`).
api (object): An API instance to target when simulating requests
(default: ``self.api_class()``). When testing your
application, you will need to overwrite this with your own
instance of ``falcon.API``, or use `api_class` to specify a
factory method for your application.
app (object): A WSGI application to target when simulating
requests (default: ``falcon.API()``). When testing your
application, you will need to set this to your own instance
of ``falcon.API``. For example::
from falcon import testing
import myapp
class MyTestCase(testing.TestCase):
def setUp(self):
super(MyTestCase, self).setUp()
# Assume the hypothetical `myapp` package has a
# function called `create()` to initialize and
# return a `falcon.API` instance.
self.app = myapp.create()
class TestMyApp(MyTestCase):
def test_get_message(self):
doc = {u'message': u'Hello world!'}
result = self.simulate_get('/messages/42')
self.assertEqual(result.json, doc)
api (object): Deprecated alias for ``app``
api_class (callable): Deprecated class variable; will be
removed in a future release.
"""
api_class = None
@property
def api(self):
return self.app
@api.setter
def api(self, value):
self.app = value
def setUp(self):
super(TestCase, self).setUp()
if self.api_class is None:
self.api = falcon.API()
app = falcon.API()
else:
self.api = self.api_class()
app = self.api_class()
# NOTE(kgriffs): Don't use super() to avoid triggering
# unittest.TestCase.__init__()
TestClient.__init__(self, app)
# Reset to simulate "restarting" the WSGI container
falcon.request._maybe_wrap_wsgi_stream = True
@@ -72,138 +107,3 @@ class TestCase(unittest.TestCase):
if not hasattr(unittest.TestCase, 'assertIn'): # pragma: nocover
def assertIn(self, a, b):
self.assertTrue(a in b)
def simulate_get(self, path='/', **kwargs):
"""Simulates a GET request to a WSGI application.
Equivalent to ``simulate_request('GET', ...)``
Args:
path (str): The URL path to request (default: '/')
Keyword Args:
query_string (str): A raw query string to include in the
request (default: ``None``)
headers (dict): Additional headers to include in the request
(default: ``None``)
"""
return client.simulate_get(self.api, path, **kwargs)
def simulate_head(self, path='/', **kwargs):
"""Simulates a HEAD request to a WSGI application.
Equivalent to ``simulate_request('HEAD', ...)``
Args:
path (str): The URL path to request (default: '/')
Keyword Args:
query_string (str): A raw query string to include in the
request (default: ``None``)
headers (dict): Additional headers to include in the request
(default: ``None``)
"""
return client.simulate_head(self.api, path, **kwargs)
def simulate_post(self, path='/', **kwargs):
"""Simulates a POST request to a WSGI application.
Equivalent to ``simulate_request('POST', ...)``
Args:
path (str): The URL path to request (default: '/')
Keyword Args:
query_string (str): A raw query string to include in the
request (default: ``None``)
headers (dict): Additional headers to include in the request
(default: ``None``)
body (str): A string to send as the body of the request.
Accepts both byte strings and Unicode strings
(default: ``None``). If a Unicode string is provided,
it will be encoded as UTF-8 in the request.
"""
return client.simulate_post(self.api, path, **kwargs)
def simulate_put(self, path='/', **kwargs):
"""Simulates a PUT request to a WSGI application.
Equivalent to ``simulate_request('PUT', ...)``
Args:
path (str): The URL path to request (default: '/')
Keyword Args:
query_string (str): A raw query string to include in the
request (default: ``None``)
headers (dict): Additional headers to include in the request
(default: ``None``)
body (str): A string to send as the body of the request.
Accepts both byte strings and Unicode strings
(default: ``None``). If a Unicode string is provided,
it will be encoded as UTF-8 in the request.
"""
return client.simulate_put(self.api, path, **kwargs)
def simulate_options(self, path='/', **kwargs):
"""Simulates an OPTIONS request to a WSGI application.
Equivalent to ``simulate_request('OPTIONS', ...)``
Args:
path (str): The URL path to request (default: '/')
Keyword Args:
query_string (str): A raw query string to include in the
request (default: ``None``)
headers (dict): Additional headers to include in the request
(default: ``None``)
"""
return client.simulate_options(self.api, path, **kwargs)
def simulate_patch(self, path='/', **kwargs):
"""Simulates a PATCH request to a WSGI application.
Equivalent to ``simulate_request('PATCH', ...)``
Args:
path (str): The URL path to request (default: '/')
Keyword Args:
query_string (str): A raw query string to include in the
request (default: ``None``)
headers (dict): Additional headers to include in the request
(default: ``None``)
body (str): A string to send as the body of the request.
Accepts both byte strings and Unicode strings
(default: ``None``). If a Unicode string is provided,
it will be encoded as UTF-8 in the request.
"""
return client.simulate_patch(self.api, path, **kwargs)
def simulate_delete(self, path='/', **kwargs):
"""Simulates a DELETE request to a WSGI application.
Equivalent to ``simulate_request('DELETE', ...)``
Args:
path (str): The URL path to request (default: '/')
Keyword Args:
query_string (str): A raw query string to include in the
request (default: ``None``)
headers (dict): Additional headers to include in the request
(default: ``None``)
"""
return client.simulate_delete(self.api, path, **kwargs)
def simulate_request(self, *args, **kwargs):
"""Simulates a request to a WSGI application.
Wraps :py:meth:`falcon.testing.simulate_request` to perform a
WSGI request directly against ``self.api``. Equivalent to::
falcon.testing.simulate_request(self.api, *args, **kwargs)
"""
return client.simulate_request(self.api, *args, **kwargs)