diff --git a/doc/api/testing.rst b/doc/api/testing.rst index 5b8b89c..c11007d 100644 --- a/doc/api/testing.rst +++ b/doc/api/testing.rst @@ -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 diff --git a/falcon/testing/__init__.py b/falcon/testing/__init__.py index d3c9a30..d390156 100644 --- a/falcon/testing/__init__.py +++ b/falcon/testing/__init__.py @@ -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 diff --git a/falcon/testing/client.py b/falcon/testing/client.py index c220d25..8c46f07 100644 --- a/falcon/testing/client.py +++ b/falcon/testing/client.py @@ -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) diff --git a/falcon/testing/test_case.py b/falcon/testing/test_case.py index d2796b8..843ac5c 100644 --- a/falcon/testing/test_case.py +++ b/falcon/testing/test_case.py @@ -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)