Login

Monkey-patch Django's test client to return WSGIRequest objects

Author:
robmadole
Posted:
November 12, 2010
Language:
Python
Version:
1.2
Score:
0 (after 0 ratings)

Testing low-level functionality sometimes requires a WSGIRequest object. An example of this is testing template tags.

This will monkey-patch the test Client object to return WSGIRequest objects

Normal Django behavior:

>>> client.get('/')
<HttpResponse >

With this code, get the request object:

>>> client.request_from.get('/')
<WSGIRequest >

Installation:

For this to work, you simply need to import the contents of this file.

If you name this file clientrequestpatch.py, do this inside your Django tests.

from django.test.testcases import TestCase
from myproject.test import clientrequestpatch
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
from functools import wraps

from django.core.handlers.wsgi import WSGIRequest
from django.core.signals import got_request_exception
from django.template import TemplateDoesNotExist
from django.utils.functional import curry
from django.test import signals
from django.test.client import Client, store_rendered_templates
from django.core.handlers.base import BaseHandler


def new_init(f):
    """
    We need to add a handler for returning the request instead of the response
    and initialize a variable that will tell us whether a particular
    get/post/put/delete is supposed to return the HttpResponse or the
    WSGIRequest
    """
    @wraps(f)
    def wrapper(self, **defaults):
        # It's an unbound function at this point, so we hook it with what we
        # get off the wrapper
        f(self, **defaults)
        self._return_request_instead = False
        self.handler_request = ClientHandler()
    return wrapper


def switch_client_request_instead(self):
    """
    Allows us to throw an internal switch in the Client to return the next
    request as WSGIRequest instead of the HttpResponse
    """
    self._return_request_instead = True
    return self


def new_request(f):
    """
    Handles actually creating a WSGIRequest instead of an HttpResponse if we
    are switched into that mode.  Will call the original request instead if
    needed.
    """
    def wrapper(self, **request):
        if not self._return_request_instead:
            # We are not supposed to return the request object itself, but do
            # the normal Django test client request
            return self.original_request(**request)

        # So we are switched into a mode where we need to return the request
        # instead of the response, we need to set the test client back to
        # response-returning normality so the next call to this client doesn't
        # continue to return WSGIRequest objects
        self._return_request_instead = False

        # Rip-off of the Django test client, but changed to return a fake WSGIRequest object
        environ = {
            'HTTP_COOKIE':       self.cookies.output(header='', sep='; '),
            'PATH_INFO':         '/',
            'QUERY_STRING':      '',
            'REMOTE_ADDR':       '127.0.0.1',
            'REQUEST_METHOD':    'GET',
            'SCRIPT_NAME':       '',
            'SERVER_NAME':       'testserver',
            'SERVER_PORT':       '80',
            'SERVER_PROTOCOL':   'HTTP/1.1',
            'wsgi.version':      (1,0),
            'wsgi.url_scheme':   'http',
            'wsgi.errors':       self.errors,
            'wsgi.multiprocess': True,
            'wsgi.multithread':  False,
            'wsgi.run_once':     False,
        }
        environ.update(self.defaults)
        environ.update(request)

        # Curry a data dictionary into an instance of the template renderer
        # callback function.
        data = {}
        on_template_render = curry(store_rendered_templates, data)
        signals.template_rendered.connect(on_template_render)

        # Capture exceptions created by the handler.
        got_request_exception.connect(self.store_exc_info)

        try:
            request = self.handler_request(environ)
        except TemplateDoesNotExist, e:
            # If the view raises an exception, Django will attempt to show
            # the 500.html template. If that template is not available,
            # we should ignore the error in favor of re-raising the
            # underlying exception that caused the 500 error. Any other
            # template found to be missing during view error handling
            # should be reported as-is.
            if e.args != ('500.html',):
                raise

        return request
    return wrapper


class ClientHandler(BaseHandler):
    """
    A HTTP Handler that can be used for testing purposes.
    Uses the WSGI interface to compose requests, but returns
    the raw HttpResponse object
    """
    def __call__(self, environ):
        from django.conf import settings
        from django.core import signals

        # Set up middleware if needed. We couldn't do this earlier, because
        # settings weren't available.
        if self._request_middleware is None:
            self.load_middleware()

        signals.request_started.send(sender=self.__class__)
        try:
            request = WSGIRequest(environ)
            response = self.get_response(request)

            # Apply response middleware.
            for middleware_method in self._response_middleware:
                response = middleware_method(request, response)
            response = self.apply_response_fixes(request, response)
        finally:
            signals.request_finished.disconnect(close_connection)
            signals.request_finished.send(sender=self.__class__)
            signals.request_finished.connect(close_connection)

        return request

# Time to do some monkey-patching.
#
# This allows us to do this with the client object
#
#   >>> client.get('/')
#   <HttpResponse >
#   >>> client.request_from.get('/')
#   <WSGIRequest >
#
setattr(Client, 'request_from', property(switch_client_request_instead))
setattr(Client, '__init__', new_init(Client.__init__))
setattr(Client, 'original_request', Client.request)
setattr(Client, 'request', new_request(Client.request))

More like this

  1. Template tag - list punctuation for a list of items by shapiromatron 10 months, 3 weeks ago
  2. JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 11 months ago
  3. Serializer factory with Django Rest Framework by julio 1 year, 5 months ago
  4. Image compression before saving the new model / work with JPG, PNG by Schleidens 1 year, 6 months ago
  5. Help text hyperlinks by sa2812 1 year, 7 months ago

Comments

Please login first before commenting.