Source code for falcon.testing.test_case

# Copyright 2013 by Rackspace Hosting, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import json
import wsgiref.validate

try:
    import testtools as unittest
except ImportError:  # pragma: nocover
    import unittest

import falcon
import falcon.request
from falcon.util import CaseInsensitiveDict
from falcon.testing.srmock import StartResponseMock
from falcon.testing.helpers import create_environ, get_encoding_from_headers


[docs]class Result(object): """Encapsulates the result of a simulated WSGI request. Args: iterable (iterable): An iterable that yields zero or more bytestrings, per PEP-3333 status (str): An HTTP status string, including status code and reason string headers (list): A list of (header_name, header_value) tuples, per PEP-3333 Attributes: status (str): HTTP status string given in the response status_code (int): The code portion of the HTTP status string headers (CaseInsensitiveDict): A case-insensitive dictionary containing all the headers in the response encoding (str): Text encoding of the response body, or ``None`` if the encoding can not be determined. content (bytes): Raw response body, or ``bytes`` if the response body was empty. text (str): Decoded response body of type ``unicode`` under Python 2.6 and 2.7, and of type ``str`` otherwise. Raises an error if the response encoding can not be determined. json (dict): Deserialized JSON body. Raises an error if the response is not JSON. """ def __init__(self, iterable, status, headers): self._text = None self._content = b''.join(iterable) if hasattr(iterable, 'close'): iterable.close() self._status = status self._status_code = int(status[:3]) self._headers = CaseInsensitiveDict(headers) self._encoding = get_encoding_from_headers(self._headers) @property def status(self): return self._status @property def status_code(self): return self._status_code @property def headers(self): return self._headers @property def encoding(self): return self._encoding @property def content(self): return self._content @property def text(self): if self._text is None: if not self.content: self._text = u'' else: if self.encoding is None: msg = 'Response did not specify a content encoding' raise RuntimeError(msg) self._text = self.content.decode(self.encoding) return self._text @property def json(self): return json.loads(self.text)
[docs]class TestCase(unittest.TestCase): """Extends :py:mod:`unittest` to support WSGI functional testing. Note: If available, uses :py:mod:`testtools` in lieu of :py:mod:`unittest`. 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`. Attributes: api_class (class): An API class 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()``) """ api_class = None def setUp(self): super(TestCase, self).setUp() if self.api_class is None: self.api = falcon.API() else: self.api = self.api_class() # Reset to simulate "restarting" the WSGI container falcon.request._maybe_wrap_wsgi_stream = True # NOTE(warsaw): Pythons earlier than 2.7 do not have a # self.assertIn() method, so use this compatibility function # instead. if not hasattr(unittest.TestCase, 'assertIn'): # pragma: nocover def assertIn(self, a, b): self.assertTrue(a in b)
[docs] 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 self.simulate_request('GET', path, **kwargs)
[docs] 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 self.simulate_request('HEAD', path, **kwargs)
[docs] 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 self.simulate_request('POST', path, **kwargs)
[docs] 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 self.simulate_request('PUT', path, **kwargs)
[docs] 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 self.simulate_request('OPTIONS', path, **kwargs)
[docs] 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 self.simulate_request('PATCH', path, **kwargs)
[docs] 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 self.simulate_request('DELETE', path, **kwargs)
[docs] def simulate_request(self, method='GET', path='/', query_string=None, headers=None, body=None, file_wrapper=None): """Simulates a request to a WSGI application. Performs a WSGI request directly against ``self.api``. Keyword Args: method (str): The HTTP method to use in the request (default: 'GET') path (str): The URL path to request (default: '/') 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. file_wrapper (callable): Callable that returns an iterable, to be used as the value for *wsgi.file_wrapper* in the environ (default: ``None``). Returns: :py:class:`~.Result`: The result of the request """ if not path.startswith('/'): raise ValueError("path must start with '/'") if query_string and query_string.startswith('?'): raise ValueError("query_string should not start with '?'") if '?' in path: # NOTE(kgriffs): We could allow this, but then we'd need # to define semantics regarding whether the path takes # precedence over the query_string. Also, it would make # tests less consistent, since there would be "more than # one...way to do it." raise ValueError( 'path may not contain a query string. Please use the ' 'query_string parameter instead.' ) env = create_environ( method=method, path=path, query_string=(query_string or ''), headers=headers, body=body, file_wrapper=file_wrapper, ) srmock = StartResponseMock() validator = wsgiref.validate.validator(self.api) iterable = validator(env, srmock) result = Result(iterable, srmock.status, srmock.headers) return result