Permalink
Browse files

Fixed #9002 -- Added a RequestFactory. This allows you to create requ…

…est instances so you can unit test views as standalone functions. Thanks to Simon Willison for the suggestion and snippet on which this patch was originally based.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@14191 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
1 parent 120aae2 commit eec45e8b710b97201db106a6460fe051f8917833 @freakboy3742 freakboy3742 committed Oct 12, 2010
Showing with 228 additions and 116 deletions.
  1. +1 −1 django/test/__init__.py
  2. +169 −114 django/test/client.py
  3. +45 −0 docs/topics/testing.txt
  4. +13 −1 tests/modeltests/test_client/models.py
@@ -2,6 +2,6 @@
Django Unit Test and Doctest framework.
"""
-from django.test.client import Client
+from django.test.client import Client, RequestFactory
from django.test.testcases import TestCase, TransactionTestCase, skipIfDBFeature, skipUnlessDBFeature
from django.test.utils import Approximate
View
@@ -156,7 +156,165 @@ def encode_file(boundary, key, file):
file.read()
]
-class Client(object):
+
+
+class RequestFactory(object):
+ """
+ Class that lets you create mock Request objects for use in testing.
+
+ Usage:
+
+ rf = RequestFactory()
+ get_request = rf.get('/hello/')
+ post_request = rf.post('/submit/', {'foo': 'bar'})
+
+ Once you have a request object you can pass it to any view function,
+ just as if that view had been hooked up using a URLconf.
+ """
+ def __init__(self, **defaults):
+ self.defaults = defaults
+ self.cookies = SimpleCookie()
+ self.errors = StringIO()
+
+ def _base_environ(self, **request):
+ """
+ The base environment for a request.
+ """
+ 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)
+ return environ
+
+ def request(self, **request):
+ "Construct a generic request object."
+ return WSGIRequest(self._base_environ(**request))
+
+ def get(self, path, data={}, **extra):
+ "Construct a GET request"
+
+ parsed = urlparse(path)
+ r = {
+ 'CONTENT_TYPE': 'text/html; charset=utf-8',
+ 'PATH_INFO': urllib.unquote(parsed[2]),
+ 'QUERY_STRING': urlencode(data, doseq=True) or parsed[4],
+ 'REQUEST_METHOD': 'GET',
+ 'wsgi.input': FakePayload('')
+ }
+ r.update(extra)
+ return self.request(**r)
+
+ def post(self, path, data={}, content_type=MULTIPART_CONTENT,
+ **extra):
+ "Construct a POST request."
+
+ if content_type is MULTIPART_CONTENT:
+ post_data = encode_multipart(BOUNDARY, data)
+ else:
+ # Encode the content so that the byte representation is correct.
+ match = CONTENT_TYPE_RE.match(content_type)
+ if match:
+ charset = match.group(1)
+ else:
+ charset = settings.DEFAULT_CHARSET
+ post_data = smart_str(data, encoding=charset)
+
+ parsed = urlparse(path)
+ r = {
+ 'CONTENT_LENGTH': len(post_data),
+ 'CONTENT_TYPE': content_type,
+ 'PATH_INFO': urllib.unquote(parsed[2]),
+ 'QUERY_STRING': parsed[4],
+ 'REQUEST_METHOD': 'POST',
+ 'wsgi.input': FakePayload(post_data),
+ }
+ r.update(extra)
+ return self.request(**r)
+
+ def head(self, path, data={}, **extra):
+ "Construct a HEAD request."
+
+ parsed = urlparse(path)
+ r = {
+ 'CONTENT_TYPE': 'text/html; charset=utf-8',
+ 'PATH_INFO': urllib.unquote(parsed[2]),
+ 'QUERY_STRING': urlencode(data, doseq=True) or parsed[4],
+ 'REQUEST_METHOD': 'HEAD',
+ 'wsgi.input': FakePayload('')
+ }
+ r.update(extra)
+ return self.request(**r)
+
+ def options(self, path, data={}, **extra):
+ "Constrict an OPTIONS request"
+
+ parsed = urlparse(path)
+ r = {
+ 'PATH_INFO': urllib.unquote(parsed[2]),
+ 'QUERY_STRING': urlencode(data, doseq=True) or parsed[4],
+ 'REQUEST_METHOD': 'OPTIONS',
+ 'wsgi.input': FakePayload('')
+ }
+ r.update(extra)
+ return self.request(**r)
+
+ def put(self, path, data={}, content_type=MULTIPART_CONTENT,
+ **extra):
+ "Construct a PUT request."
+
+ if content_type is MULTIPART_CONTENT:
+ post_data = encode_multipart(BOUNDARY, data)
+ else:
+ post_data = data
+
+ # Make `data` into a querystring only if it's not already a string. If
+ # it is a string, we'll assume that the caller has already encoded it.
+ query_string = None
+ if not isinstance(data, basestring):
+ query_string = urlencode(data, doseq=True)
+
+ parsed = urlparse(path)
+ r = {
+ 'CONTENT_LENGTH': len(post_data),
+ 'CONTENT_TYPE': content_type,
+ 'PATH_INFO': urllib.unquote(parsed[2]),
+ 'QUERY_STRING': query_string or parsed[4],
+ 'REQUEST_METHOD': 'PUT',
+ 'wsgi.input': FakePayload(post_data),
+ }
+ r.update(extra)
+ return self.request(**r)
+
+ def delete(self, path, data={}, **extra):
+ "Construct a DELETE request."
+
+ parsed = urlparse(path)
+ r = {
+ 'PATH_INFO': urllib.unquote(parsed[2]),
+ 'QUERY_STRING': urlencode(data, doseq=True) or parsed[4],
+ 'REQUEST_METHOD': 'DELETE',
+ 'wsgi.input': FakePayload('')
+ }
+ r.update(extra)
+ return self.request(**r)
+
+
+class Client(RequestFactory):
"""
A class that can act as a client for testing purposes.
@@ -175,11 +333,9 @@ class Client(object):
HTML rendered to the end-user.
"""
def __init__(self, enforce_csrf_checks=False, **defaults):
+ super(Client, self).__init__(**defaults)
self.handler = ClientHandler(enforce_csrf_checks)
- self.defaults = defaults
- self.cookies = SimpleCookie()
self.exc_info = None
- self.errors = StringIO()
def store_exc_info(self, **kwargs):
"""
@@ -199,32 +355,15 @@ def _session(self):
return {}
session = property(_session)
+
def request(self, **request):
"""
The master request method. Composes the environment dictionary
and passes to the handler, returning the result of the handler.
Assumes defaults for the query environment, which can be overridden
using the arguments to the request.
"""
- 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)
+ environ = self._base_environ(**request)
# Curry a data dictionary into an instance of the template renderer
# callback function.
@@ -290,22 +429,11 @@ def _get_template(self):
signals.template_rendered.disconnect(dispatch_uid="template-render")
got_request_exception.disconnect(dispatch_uid="request-exception")
-
def get(self, path, data={}, follow=False, **extra):
"""
Requests a response from the server using GET.
"""
- parsed = urlparse(path)
- r = {
- 'CONTENT_TYPE': 'text/html; charset=utf-8',
- 'PATH_INFO': urllib.unquote(parsed[2]),
- 'QUERY_STRING': urlencode(data, doseq=True) or parsed[4],
- 'REQUEST_METHOD': 'GET',
- 'wsgi.input': FakePayload('')
- }
- r.update(extra)
-
- response = self.request(**r)
+ response = super(Client, self).get(path, data=data, **extra)
if follow:
response = self._handle_redirects(response, **extra)
return response
@@ -315,29 +443,7 @@ def post(self, path, data={}, content_type=MULTIPART_CONTENT,
"""
Requests a response from the server using POST.
"""
- if content_type is MULTIPART_CONTENT:
- post_data = encode_multipart(BOUNDARY, data)
- else:
- # Encode the content so that the byte representation is correct.
- match = CONTENT_TYPE_RE.match(content_type)
- if match:
- charset = match.group(1)
- else:
- charset = settings.DEFAULT_CHARSET
- post_data = smart_str(data, encoding=charset)
-
- parsed = urlparse(path)
- r = {
- 'CONTENT_LENGTH': len(post_data),
- 'CONTENT_TYPE': content_type,
- 'PATH_INFO': urllib.unquote(parsed[2]),
- 'QUERY_STRING': parsed[4],
- 'REQUEST_METHOD': 'POST',
- 'wsgi.input': FakePayload(post_data),
- }
- r.update(extra)
-
- response = self.request(**r)
+ response = super(Client, self).post(path, data=data, content_type=content_type, **extra)
if follow:
response = self._handle_redirects(response, **extra)
return response
@@ -346,17 +452,7 @@ def head(self, path, data={}, follow=False, **extra):
"""
Request a response from the server using HEAD.
"""
- parsed = urlparse(path)
- r = {
- 'CONTENT_TYPE': 'text/html; charset=utf-8',
- 'PATH_INFO': urllib.unquote(parsed[2]),
- 'QUERY_STRING': urlencode(data, doseq=True) or parsed[4],
- 'REQUEST_METHOD': 'HEAD',
- 'wsgi.input': FakePayload('')
- }
- r.update(extra)
-
- response = self.request(**r)
+ response = super(Client, self).head(path, data=data, **extra)
if follow:
response = self._handle_redirects(response, **extra)
return response
@@ -365,16 +461,7 @@ def options(self, path, data={}, follow=False, **extra):
"""
Request a response from the server using OPTIONS.
"""
- parsed = urlparse(path)
- r = {
- 'PATH_INFO': urllib.unquote(parsed[2]),
- 'QUERY_STRING': urlencode(data, doseq=True) or parsed[4],
- 'REQUEST_METHOD': 'OPTIONS',
- 'wsgi.input': FakePayload('')
- }
- r.update(extra)
-
- response = self.request(**r)
+ response = super(Client, self).options(path, data=data, **extra)
if follow:
response = self._handle_redirects(response, **extra)
return response
@@ -384,29 +471,7 @@ def put(self, path, data={}, content_type=MULTIPART_CONTENT,
"""
Send a resource to the server using PUT.
"""
- if content_type is MULTIPART_CONTENT:
- post_data = encode_multipart(BOUNDARY, data)
- else:
- post_data = data
-
- # Make `data` into a querystring only if it's not already a string. If
- # it is a string, we'll assume that the caller has already encoded it.
- query_string = None
- if not isinstance(data, basestring):
- query_string = urlencode(data, doseq=True)
-
- parsed = urlparse(path)
- r = {
- 'CONTENT_LENGTH': len(post_data),
- 'CONTENT_TYPE': content_type,
- 'PATH_INFO': urllib.unquote(parsed[2]),
- 'QUERY_STRING': query_string or parsed[4],
- 'REQUEST_METHOD': 'PUT',
- 'wsgi.input': FakePayload(post_data),
- }
- r.update(extra)
-
- response = self.request(**r)
+ response = super(Client, self).put(path, data=data, content_type=content_type, **extra)
if follow:
response = self._handle_redirects(response, **extra)
return response
@@ -415,23 +480,14 @@ def delete(self, path, data={}, follow=False, **extra):
"""
Send a DELETE request to the server.
"""
- parsed = urlparse(path)
- r = {
- 'PATH_INFO': urllib.unquote(parsed[2]),
- 'QUERY_STRING': urlencode(data, doseq=True) or parsed[4],
- 'REQUEST_METHOD': 'DELETE',
- 'wsgi.input': FakePayload('')
- }
- r.update(extra)
-
- response = self.request(**r)
+ response = super(Client, self).delete(path, data=data, **extra)
if follow:
response = self._handle_redirects(response, **extra)
return response
def login(self, **credentials):
"""
- Sets the Client to appear as if it has successfully logged into a site.
+ Sets the Factory to appear as if it has successfully logged into a site.
Returns True if login is possible; False if the provided credentials
are incorrect, or the user is inactive, or if the sessions framework is
@@ -506,4 +562,3 @@ def _handle_redirects(self, response, **extra):
if response.redirect_chain[-1] in response.redirect_chain[0:-1]:
break
return response
-
Oops, something went wrong.

0 comments on commit eec45e8

Please sign in to comment.