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:
committed by
John Vrbanac
parent
2b63419867
commit
fb7ca62fea
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user