Fixed #4476 -- Added a ``follow`` option to the test client request methods. This implements browser-like behavior for the test client, following redirect chains when a 30X response is received. Thanks to Marc Fargas and Keith Bussell for their work on this.
git-svn-id: http://code.djangoproject.com/svn/django/trunk@9911 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
parent
e20f09c2d0
commit
e735fe7160
|
@ -1,5 +1,5 @@
|
||||||
import urllib
|
import urllib
|
||||||
from urlparse import urlparse, urlunparse
|
from urlparse import urlparse, urlunparse, urlsplit
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
try:
|
try:
|
||||||
|
@ -12,7 +12,7 @@ from django.contrib.auth import authenticate, login
|
||||||
from django.core.handlers.base import BaseHandler
|
from django.core.handlers.base import BaseHandler
|
||||||
from django.core.handlers.wsgi import WSGIRequest
|
from django.core.handlers.wsgi import WSGIRequest
|
||||||
from django.core.signals import got_request_exception
|
from django.core.signals import got_request_exception
|
||||||
from django.http import SimpleCookie, HttpRequest
|
from django.http import SimpleCookie, HttpRequest, QueryDict
|
||||||
from django.template import TemplateDoesNotExist
|
from django.template import TemplateDoesNotExist
|
||||||
from django.test import signals
|
from django.test import signals
|
||||||
from django.utils.functional import curry
|
from django.utils.functional import curry
|
||||||
|
@ -261,7 +261,7 @@ class Client(object):
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def get(self, path, data={}, **extra):
|
def get(self, path, data={}, follow=False, **extra):
|
||||||
"""
|
"""
|
||||||
Requests a response from the server using GET.
|
Requests a response from the server using GET.
|
||||||
"""
|
"""
|
||||||
|
@ -275,9 +275,13 @@ class Client(object):
|
||||||
}
|
}
|
||||||
r.update(extra)
|
r.update(extra)
|
||||||
|
|
||||||
return self.request(**r)
|
response = self.request(**r)
|
||||||
|
if follow:
|
||||||
|
response = self._handle_redirects(response)
|
||||||
|
return response
|
||||||
|
|
||||||
def post(self, path, data={}, content_type=MULTIPART_CONTENT, **extra):
|
def post(self, path, data={}, content_type=MULTIPART_CONTENT,
|
||||||
|
follow=False, **extra):
|
||||||
"""
|
"""
|
||||||
Requests a response from the server using POST.
|
Requests a response from the server using POST.
|
||||||
"""
|
"""
|
||||||
|
@ -297,9 +301,12 @@ class Client(object):
|
||||||
}
|
}
|
||||||
r.update(extra)
|
r.update(extra)
|
||||||
|
|
||||||
return self.request(**r)
|
response = self.request(**r)
|
||||||
|
if follow:
|
||||||
|
response = self._handle_redirects(response)
|
||||||
|
return response
|
||||||
|
|
||||||
def head(self, path, data={}, **extra):
|
def head(self, path, data={}, follow=False, **extra):
|
||||||
"""
|
"""
|
||||||
Request a response from the server using HEAD.
|
Request a response from the server using HEAD.
|
||||||
"""
|
"""
|
||||||
|
@ -313,9 +320,12 @@ class Client(object):
|
||||||
}
|
}
|
||||||
r.update(extra)
|
r.update(extra)
|
||||||
|
|
||||||
return self.request(**r)
|
response = self.request(**r)
|
||||||
|
if follow:
|
||||||
|
response = self._handle_redirects(response)
|
||||||
|
return response
|
||||||
|
|
||||||
def options(self, path, data={}, **extra):
|
def options(self, path, data={}, follow=False, **extra):
|
||||||
"""
|
"""
|
||||||
Request a response from the server using OPTIONS.
|
Request a response from the server using OPTIONS.
|
||||||
"""
|
"""
|
||||||
|
@ -328,9 +338,13 @@ class Client(object):
|
||||||
}
|
}
|
||||||
r.update(extra)
|
r.update(extra)
|
||||||
|
|
||||||
return self.request(**r)
|
response = self.request(**r)
|
||||||
|
if follow:
|
||||||
|
response = self._handle_redirects(response)
|
||||||
|
return response
|
||||||
|
|
||||||
def put(self, path, data={}, content_type=MULTIPART_CONTENT, **extra):
|
def put(self, path, data={}, content_type=MULTIPART_CONTENT,
|
||||||
|
follow=False, **extra):
|
||||||
"""
|
"""
|
||||||
Send a resource to the server using PUT.
|
Send a resource to the server using PUT.
|
||||||
"""
|
"""
|
||||||
|
@ -350,9 +364,12 @@ class Client(object):
|
||||||
}
|
}
|
||||||
r.update(extra)
|
r.update(extra)
|
||||||
|
|
||||||
return self.request(**r)
|
response = self.request(**r)
|
||||||
|
if follow:
|
||||||
|
response = self._handle_redirects(response)
|
||||||
|
return response
|
||||||
|
|
||||||
def delete(self, path, data={}, **extra):
|
def delete(self, path, data={}, follow=False, **extra):
|
||||||
"""
|
"""
|
||||||
Send a DELETE request to the server.
|
Send a DELETE request to the server.
|
||||||
"""
|
"""
|
||||||
|
@ -365,7 +382,10 @@ class Client(object):
|
||||||
}
|
}
|
||||||
r.update(extra)
|
r.update(extra)
|
||||||
|
|
||||||
return self.request(**r)
|
response = self.request(**r)
|
||||||
|
if follow:
|
||||||
|
response = self._handle_redirects(response)
|
||||||
|
return response
|
||||||
|
|
||||||
def login(self, **credentials):
|
def login(self, **credentials):
|
||||||
"""
|
"""
|
||||||
|
@ -416,3 +436,27 @@ class Client(object):
|
||||||
session = __import__(settings.SESSION_ENGINE, {}, {}, ['']).SessionStore()
|
session = __import__(settings.SESSION_ENGINE, {}, {}, ['']).SessionStore()
|
||||||
session.delete(session_key=self.cookies[settings.SESSION_COOKIE_NAME].value)
|
session.delete(session_key=self.cookies[settings.SESSION_COOKIE_NAME].value)
|
||||||
self.cookies = SimpleCookie()
|
self.cookies = SimpleCookie()
|
||||||
|
|
||||||
|
def _handle_redirects(self, response):
|
||||||
|
"Follows any redirects by requesting responses from the server using GET."
|
||||||
|
|
||||||
|
response.redirect_chain = []
|
||||||
|
while response.status_code in (301, 302, 303, 307):
|
||||||
|
url = response['Location']
|
||||||
|
scheme, netloc, path, query, fragment = urlsplit(url)
|
||||||
|
|
||||||
|
redirect_chain = response.redirect_chain
|
||||||
|
redirect_chain.append((url, response.status_code))
|
||||||
|
|
||||||
|
# The test client doesn't handle external links,
|
||||||
|
# but since the situation is simulated in test_client,
|
||||||
|
# we fake things here by ignoring the netloc portion of the
|
||||||
|
# redirected URL.
|
||||||
|
response = self.get(path, QueryDict(query), follow=False)
|
||||||
|
response.redirect_chain = redirect_chain
|
||||||
|
|
||||||
|
# Prevent loops
|
||||||
|
if response.redirect_chain[-1] in response.redirect_chain[0:-1]:
|
||||||
|
break
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
|
@ -43,7 +43,7 @@ def disable_transaction_methods():
|
||||||
transaction.savepoint_commit = nop
|
transaction.savepoint_commit = nop
|
||||||
transaction.savepoint_rollback = nop
|
transaction.savepoint_rollback = nop
|
||||||
transaction.enter_transaction_management = nop
|
transaction.enter_transaction_management = nop
|
||||||
transaction.leave_transaction_management = nop
|
transaction.leave_transaction_management = nop
|
||||||
|
|
||||||
def restore_transaction_methods():
|
def restore_transaction_methods():
|
||||||
transaction.commit = real_commit
|
transaction.commit = real_commit
|
||||||
|
@ -198,7 +198,7 @@ class DocTestRunner(doctest.DocTestRunner):
|
||||||
# Rollback, in case of database errors. Otherwise they'd have
|
# Rollback, in case of database errors. Otherwise they'd have
|
||||||
# side effects on other tests.
|
# side effects on other tests.
|
||||||
transaction.rollback_unless_managed()
|
transaction.rollback_unless_managed()
|
||||||
|
|
||||||
class TransactionTestCase(unittest.TestCase):
|
class TransactionTestCase(unittest.TestCase):
|
||||||
def _pre_setup(self):
|
def _pre_setup(self):
|
||||||
"""Performs any pre-test setup. This includes:
|
"""Performs any pre-test setup. This includes:
|
||||||
|
@ -242,7 +242,7 @@ class TransactionTestCase(unittest.TestCase):
|
||||||
import sys
|
import sys
|
||||||
result.addError(self, sys.exc_info())
|
result.addError(self, sys.exc_info())
|
||||||
return
|
return
|
||||||
super(TransactionTestCase, self).__call__(result)
|
super(TransactionTestCase, self).__call__(result)
|
||||||
try:
|
try:
|
||||||
self._post_teardown()
|
self._post_teardown()
|
||||||
except (KeyboardInterrupt, SystemExit):
|
except (KeyboardInterrupt, SystemExit):
|
||||||
|
@ -263,7 +263,7 @@ class TransactionTestCase(unittest.TestCase):
|
||||||
def _fixture_teardown(self):
|
def _fixture_teardown(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _urlconf_teardown(self):
|
def _urlconf_teardown(self):
|
||||||
if hasattr(self, '_old_root_urlconf'):
|
if hasattr(self, '_old_root_urlconf'):
|
||||||
settings.ROOT_URLCONF = self._old_root_urlconf
|
settings.ROOT_URLCONF = self._old_root_urlconf
|
||||||
clear_url_caches()
|
clear_url_caches()
|
||||||
|
@ -276,25 +276,48 @@ class TransactionTestCase(unittest.TestCase):
|
||||||
Note that assertRedirects won't work for external links since it uses
|
Note that assertRedirects won't work for external links since it uses
|
||||||
TestClient to do a request.
|
TestClient to do a request.
|
||||||
"""
|
"""
|
||||||
self.assertEqual(response.status_code, status_code,
|
if hasattr(response, 'redirect_chain'):
|
||||||
("Response didn't redirect as expected: Response code was %d"
|
# The request was a followed redirect
|
||||||
" (expected %d)" % (response.status_code, status_code)))
|
self.assertTrue(len(response.redirect_chain) > 0,
|
||||||
url = response['Location']
|
("Response didn't redirect as expected: Response code was %d"
|
||||||
scheme, netloc, path, query, fragment = urlsplit(url)
|
" (expected %d)" % (response.status_code, status_code)))
|
||||||
|
|
||||||
|
self.assertEqual(response.redirect_chain[0][1], status_code,
|
||||||
|
("Initial response didn't redirect as expected: Response code was %d"
|
||||||
|
" (expected %d)" % (response.redirect_chain[0][1], status_code)))
|
||||||
|
|
||||||
|
url, status_code = response.redirect_chain[-1]
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, target_status_code,
|
||||||
|
("Response didn't redirect as expected: Final Response code was %d"
|
||||||
|
" (expected %d)" % (response.status_code, target_status_code)))
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Not a followed redirect
|
||||||
|
self.assertEqual(response.status_code, status_code,
|
||||||
|
("Response didn't redirect as expected: Response code was %d"
|
||||||
|
" (expected %d)" % (response.status_code, status_code)))
|
||||||
|
|
||||||
|
url = response['Location']
|
||||||
|
scheme, netloc, path, query, fragment = urlsplit(url)
|
||||||
|
|
||||||
|
redirect_response = response.client.get(path, QueryDict(query))
|
||||||
|
|
||||||
|
# Get the redirection page, using the same client that was used
|
||||||
|
# to obtain the original response.
|
||||||
|
self.assertEqual(redirect_response.status_code, target_status_code,
|
||||||
|
("Couldn't retrieve redirection page '%s': response code was %d"
|
||||||
|
" (expected %d)") %
|
||||||
|
(path, redirect_response.status_code, target_status_code))
|
||||||
|
|
||||||
e_scheme, e_netloc, e_path, e_query, e_fragment = urlsplit(expected_url)
|
e_scheme, e_netloc, e_path, e_query, e_fragment = urlsplit(expected_url)
|
||||||
if not (e_scheme or e_netloc):
|
if not (e_scheme or e_netloc):
|
||||||
expected_url = urlunsplit(('http', host or 'testserver', e_path,
|
expected_url = urlunsplit(('http', host or 'testserver', e_path,
|
||||||
e_query, e_fragment))
|
e_query, e_fragment))
|
||||||
|
|
||||||
self.assertEqual(url, expected_url,
|
self.assertEqual(url, expected_url,
|
||||||
"Response redirected to '%s', expected '%s'" % (url, expected_url))
|
"Response redirected to '%s', expected '%s'" % (url, expected_url))
|
||||||
|
|
||||||
# Get the redirection page, using the same client that was used
|
|
||||||
# to obtain the original response.
|
|
||||||
redirect_response = response.client.get(path, QueryDict(query))
|
|
||||||
self.assertEqual(redirect_response.status_code, target_status_code,
|
|
||||||
("Couldn't retrieve redirection page '%s': response code was %d"
|
|
||||||
" (expected %d)") %
|
|
||||||
(path, redirect_response.status_code, target_status_code))
|
|
||||||
|
|
||||||
def assertContains(self, response, text, count=None, status_code=200):
|
def assertContains(self, response, text, count=None, status_code=200):
|
||||||
"""
|
"""
|
||||||
|
@ -401,15 +424,15 @@ class TransactionTestCase(unittest.TestCase):
|
||||||
class TestCase(TransactionTestCase):
|
class TestCase(TransactionTestCase):
|
||||||
"""
|
"""
|
||||||
Does basically the same as TransactionTestCase, but surrounds every test
|
Does basically the same as TransactionTestCase, but surrounds every test
|
||||||
with a transaction, monkey-patches the real transaction management routines to
|
with a transaction, monkey-patches the real transaction management routines to
|
||||||
do nothing, and rollsback the test transaction at the end of the test. You have
|
do nothing, and rollsback the test transaction at the end of the test. You have
|
||||||
to use TransactionTestCase, if you need transaction management inside a test.
|
to use TransactionTestCase, if you need transaction management inside a test.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def _fixture_setup(self):
|
def _fixture_setup(self):
|
||||||
if not settings.DATABASE_SUPPORTS_TRANSACTIONS:
|
if not settings.DATABASE_SUPPORTS_TRANSACTIONS:
|
||||||
return super(TestCase, self)._fixture_setup()
|
return super(TestCase, self)._fixture_setup()
|
||||||
|
|
||||||
transaction.enter_transaction_management()
|
transaction.enter_transaction_management()
|
||||||
transaction.managed(True)
|
transaction.managed(True)
|
||||||
disable_transaction_methods()
|
disable_transaction_methods()
|
||||||
|
@ -426,7 +449,7 @@ class TestCase(TransactionTestCase):
|
||||||
def _fixture_teardown(self):
|
def _fixture_teardown(self):
|
||||||
if not settings.DATABASE_SUPPORTS_TRANSACTIONS:
|
if not settings.DATABASE_SUPPORTS_TRANSACTIONS:
|
||||||
return super(TestCase, self)._fixture_teardown()
|
return super(TestCase, self)._fixture_teardown()
|
||||||
|
|
||||||
restore_transaction_methods()
|
restore_transaction_methods()
|
||||||
transaction.rollback()
|
transaction.rollback()
|
||||||
transaction.leave_transaction_management()
|
transaction.leave_transaction_management()
|
||||||
|
|
|
@ -478,7 +478,8 @@ arguments at time of construction:
|
||||||
Once you have a ``Client`` instance, you can call any of the following
|
Once you have a ``Client`` instance, you can call any of the following
|
||||||
methods:
|
methods:
|
||||||
|
|
||||||
.. method:: Client.get(path, data={})
|
.. method:: Client.get(path, data={}, follow=False)
|
||||||
|
|
||||||
|
|
||||||
Makes a GET request on the provided ``path`` and returns a ``Response``
|
Makes a GET request on the provided ``path`` and returns a ``Response``
|
||||||
object, which is documented below.
|
object, which is documented below.
|
||||||
|
@ -505,7 +506,18 @@ arguments at time of construction:
|
||||||
If you provide URL both an encoded GET data and a data argument,
|
If you provide URL both an encoded GET data and a data argument,
|
||||||
the data argument will take precedence.
|
the data argument will take precedence.
|
||||||
|
|
||||||
.. method:: Client.post(path, data={}, content_type=MULTIPART_CONTENT)
|
If you set ``follow`` to ``True`` the client will follow any redirects
|
||||||
|
and a ``redirect_chain`` attribute will be set in the response object
|
||||||
|
containing tuples of the intermediate urls and status codes.
|
||||||
|
|
||||||
|
If you had an url ``/redirect_me/`` that redirected to ``/next/``, that
|
||||||
|
redirected to ``/final/``, this is what you'd see::
|
||||||
|
|
||||||
|
>>> response = c.get('/redirect_me/')
|
||||||
|
>>> response.redirect_chain
|
||||||
|
[(u'http://testserver/next/', 302), (u'http://testserver/final/', 302)]
|
||||||
|
|
||||||
|
.. method:: Client.post(path, data={}, content_type=MULTIPART_CONTENT, follow=False)
|
||||||
|
|
||||||
Makes a POST request on the provided ``path`` and returns a
|
Makes a POST request on the provided ``path`` and returns a
|
||||||
``Response`` object, which is documented below.
|
``Response`` object, which is documented below.
|
||||||
|
@ -556,7 +568,7 @@ arguments at time of construction:
|
||||||
Note that you should manually close the file after it has been provided
|
Note that you should manually close the file after it has been provided
|
||||||
to ``post()``.
|
to ``post()``.
|
||||||
|
|
||||||
.. versionadded:: development
|
.. versionchanged:: 1.1
|
||||||
|
|
||||||
If the URL you request with a POST contains encoded parameters, these
|
If the URL you request with a POST contains encoded parameters, these
|
||||||
parameters will be made available in the request.GET data. For example,
|
parameters will be made available in the request.GET data. For example,
|
||||||
|
@ -568,7 +580,11 @@ arguments at time of construction:
|
||||||
to retrieve the username and password, and could interrogate request.GET
|
to retrieve the username and password, and could interrogate request.GET
|
||||||
to determine if the user was a visitor.
|
to determine if the user was a visitor.
|
||||||
|
|
||||||
.. method:: Client.head(path, data={})
|
If you set ``follow`` to ``True`` the client will follow any redirects
|
||||||
|
and a ``redirect_chain`` attribute will be set in the response object
|
||||||
|
containing tuples of the intermediate urls and status codes.
|
||||||
|
|
||||||
|
.. method:: Client.head(path, data={}, follow=False)
|
||||||
|
|
||||||
.. versionadded:: development
|
.. versionadded:: development
|
||||||
|
|
||||||
|
@ -576,14 +592,22 @@ arguments at time of construction:
|
||||||
object. Useful for testing RESTful interfaces. Acts just like
|
object. Useful for testing RESTful interfaces. Acts just like
|
||||||
:meth:`Client.get` except it does not return a message body.
|
:meth:`Client.get` except it does not return a message body.
|
||||||
|
|
||||||
.. method:: Client.options(path, data={})
|
If you set ``follow`` to ``True`` the client will follow any redirects
|
||||||
|
and a ``redirect_chain`` attribute will be set in the response object
|
||||||
|
containing tuples of the intermediate urls and status codes.
|
||||||
|
|
||||||
|
.. method:: Client.options(path, data={}, follow=False)
|
||||||
|
|
||||||
.. versionadded:: development
|
.. versionadded:: development
|
||||||
|
|
||||||
Makes an OPTIONS request on the provided ``path`` and returns a
|
Makes an OPTIONS request on the provided ``path`` and returns a
|
||||||
``Response`` object. Useful for testing RESTful interfaces.
|
``Response`` object. Useful for testing RESTful interfaces.
|
||||||
|
|
||||||
.. method:: Client.put(path, data={}, content_type=MULTIPART_CONTENT)
|
If you set ``follow`` to ``True`` the client will follow any redirects
|
||||||
|
and a ``redirect_chain`` attribute will be set in the response object
|
||||||
|
containing tuples of the intermediate urls and status codes.
|
||||||
|
|
||||||
|
.. method:: Client.put(path, data={}, content_type=MULTIPART_CONTENT, follow=False)
|
||||||
|
|
||||||
.. versionadded:: development
|
.. versionadded:: development
|
||||||
|
|
||||||
|
@ -591,13 +615,21 @@ arguments at time of construction:
|
||||||
``Response`` object. Useful for testing RESTful interfaces. Acts just
|
``Response`` object. Useful for testing RESTful interfaces. Acts just
|
||||||
like :meth:`Client.post` except with the PUT request method.
|
like :meth:`Client.post` except with the PUT request method.
|
||||||
|
|
||||||
.. method:: Client.delete(path)
|
If you set ``follow`` to ``True`` the client will follow any redirects
|
||||||
|
and a ``redirect_chain`` attribute will be set in the response object
|
||||||
|
containing tuples of the intermediate urls and status codes.
|
||||||
|
|
||||||
|
.. method:: Client.delete(path, follow=False)
|
||||||
|
|
||||||
.. versionadded:: development
|
.. versionadded:: development
|
||||||
|
|
||||||
Makes an DELETE request on the provided ``path`` and returns a
|
Makes an DELETE request on the provided ``path`` and returns a
|
||||||
``Response`` object. Useful for testing RESTful interfaces.
|
``Response`` object. Useful for testing RESTful interfaces.
|
||||||
|
|
||||||
|
If you set ``follow`` to ``True`` the client will follow any redirects
|
||||||
|
and a ``redirect_chain`` attribute will be set in the response object
|
||||||
|
containing tuples of the intermediate urls and status codes.
|
||||||
|
|
||||||
.. method:: Client.login(**credentials)
|
.. method:: Client.login(**credentials)
|
||||||
|
|
||||||
.. versionadded:: 1.0
|
.. versionadded:: 1.0
|
||||||
|
@ -789,47 +821,47 @@ additions.
|
||||||
|
|
||||||
.. class:: TransactionTestCase()
|
.. class:: TransactionTestCase()
|
||||||
|
|
||||||
Django ``TestCase`` classes make use of database transaction facilities, if
|
Django ``TestCase`` classes make use of database transaction facilities, if
|
||||||
available, to speed up the process of resetting the database to a known state
|
available, to speed up the process of resetting the database to a known state
|
||||||
at the beginning of each test. A consequence of this, however, is that the
|
at the beginning of each test. A consequence of this, however, is that the
|
||||||
effects of transaction commit and rollback cannot be tested by a Django
|
effects of transaction commit and rollback cannot be tested by a Django
|
||||||
``TestCase`` class. If your test requires testing of such transactional
|
``TestCase`` class. If your test requires testing of such transactional
|
||||||
behavior, you should use a Django ``TransactionTestCase``.
|
behavior, you should use a Django ``TransactionTestCase``.
|
||||||
|
|
||||||
``TransactionTestCase`` and ``TestCase`` are identical except for the manner
|
``TransactionTestCase`` and ``TestCase`` are identical except for the manner
|
||||||
in which the database is reset to a known state and the ability for test code
|
in which the database is reset to a known state and the ability for test code
|
||||||
to test the effects of commit and rollback. A ``TranscationTestCase`` resets
|
to test the effects of commit and rollback. A ``TranscationTestCase`` resets
|
||||||
the database before the test runs by truncating all tables and reloading
|
the database before the test runs by truncating all tables and reloading
|
||||||
initial data. A ``TransactionTestCase`` may call commit and rollback and
|
initial data. A ``TransactionTestCase`` may call commit and rollback and
|
||||||
observe the effects of these calls on the database.
|
observe the effects of these calls on the database.
|
||||||
|
|
||||||
A ``TestCase``, on the other hand, does not truncate tables and reload initial
|
A ``TestCase``, on the other hand, does not truncate tables and reload initial
|
||||||
data at the beginning of a test. Instead, it encloses the test code in a
|
data at the beginning of a test. Instead, it encloses the test code in a
|
||||||
database transaction that is rolled back at the end of the test. It also
|
database transaction that is rolled back at the end of the test. It also
|
||||||
prevents the code under test from issuing any commit or rollback operations
|
prevents the code under test from issuing any commit or rollback operations
|
||||||
on the database, to ensure that the rollback at the end of the test restores
|
on the database, to ensure that the rollback at the end of the test restores
|
||||||
the database to its initial state. In order to guarantee that all ``TestCase``
|
the database to its initial state. In order to guarantee that all ``TestCase``
|
||||||
code starts with a clean database, the Django test runner runs all ``TestCase``
|
code starts with a clean database, the Django test runner runs all ``TestCase``
|
||||||
tests first, before any other tests (e.g. doctests) that may alter the
|
tests first, before any other tests (e.g. doctests) that may alter the
|
||||||
database without restoring it to its original state.
|
database without restoring it to its original state.
|
||||||
|
|
||||||
When running on a database that does not support rollback (e.g. MySQL with the
|
When running on a database that does not support rollback (e.g. MySQL with the
|
||||||
MyISAM storage engine), ``TestCase`` falls back to initializing the database
|
MyISAM storage engine), ``TestCase`` falls back to initializing the database
|
||||||
by truncating tables and reloading initial data.
|
by truncating tables and reloading initial data.
|
||||||
|
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
The ``TestCase`` use of rollback to un-do the effects of the test code
|
The ``TestCase`` use of rollback to un-do the effects of the test code
|
||||||
may reveal previously-undetected errors in test code. For example,
|
may reveal previously-undetected errors in test code. For example,
|
||||||
test code that assumes primary keys values will be assigned starting at
|
test code that assumes primary keys values will be assigned starting at
|
||||||
one may find that assumption no longer holds true when rollbacks instead
|
one may find that assumption no longer holds true when rollbacks instead
|
||||||
of table truncation are being used to reset the database. Similarly,
|
of table truncation are being used to reset the database. Similarly,
|
||||||
the reordering of tests so that all ``TestCase`` classes run first may
|
the reordering of tests so that all ``TestCase`` classes run first may
|
||||||
reveal unexpected dependencies on test case ordering. In such cases a
|
reveal unexpected dependencies on test case ordering. In such cases a
|
||||||
quick fix is to switch the ``TestCase`` to a ``TransactionTestCase``.
|
quick fix is to switch the ``TestCase`` to a ``TransactionTestCase``.
|
||||||
A better long-term fix, that allows the test to take advantage of the
|
A better long-term fix, that allows the test to take advantage of the
|
||||||
speed benefit of ``TestCase``, is to fix the underlying test problem.
|
speed benefit of ``TestCase``, is to fix the underlying test problem.
|
||||||
|
|
||||||
|
|
||||||
Default test client
|
Default test client
|
||||||
~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~
|
||||||
|
@ -1028,9 +1060,15 @@ applications:
|
||||||
.. method:: assertRedirects(response, expected_url, status_code=302, target_status_code=200)
|
.. method:: assertRedirects(response, expected_url, status_code=302, target_status_code=200)
|
||||||
|
|
||||||
Asserts that the response return a ``status_code`` redirect status, it
|
Asserts that the response return a ``status_code`` redirect status, it
|
||||||
redirected to ``expected_url`` (including any GET data), and the subsequent
|
redirected to ``expected_url`` (including any GET data), and the final
|
||||||
page was received with ``target_status_code``.
|
page was received with ``target_status_code``.
|
||||||
|
|
||||||
|
.. versionadded:: 1.1
|
||||||
|
|
||||||
|
If your request used the ``follow`` argument, the ``expected_url`` and
|
||||||
|
``target_status_code`` will be the url and status code for the final
|
||||||
|
point of the redirect chain.
|
||||||
|
|
||||||
E-mail services
|
E-mail services
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
|
|
|
@ -70,13 +70,13 @@ class ClientTest(TestCase):
|
||||||
self.assertEqual(response.context['data'], '37')
|
self.assertEqual(response.context['data'], '37')
|
||||||
self.assertEqual(response.template.name, 'POST Template')
|
self.assertEqual(response.template.name, 'POST Template')
|
||||||
self.failUnless('Data received' in response.content)
|
self.failUnless('Data received' in response.content)
|
||||||
|
|
||||||
def test_response_headers(self):
|
def test_response_headers(self):
|
||||||
"Check the value of HTTP headers returned in a response"
|
"Check the value of HTTP headers returned in a response"
|
||||||
response = self.client.get("/test_client/header_view/")
|
response = self.client.get("/test_client/header_view/")
|
||||||
|
|
||||||
self.assertEquals(response['X-DJANGO-TEST'], 'Slartibartfast')
|
self.assertEquals(response['X-DJANGO-TEST'], 'Slartibartfast')
|
||||||
|
|
||||||
def test_raw_post(self):
|
def test_raw_post(self):
|
||||||
"POST raw data (with a content type) to a view"
|
"POST raw data (with a content type) to a view"
|
||||||
test_doc = """<?xml version="1.0" encoding="utf-8"?><library><book><title>Blink</title><author>Malcolm Gladwell</author></book></library>"""
|
test_doc = """<?xml version="1.0" encoding="utf-8"?><library><book><title>Blink</title><author>Malcolm Gladwell</author></book></library>"""
|
||||||
|
@ -132,6 +132,12 @@ class ClientTest(TestCase):
|
||||||
# the attempt to get the redirection location returned 301 when retrieved
|
# the attempt to get the redirection location returned 301 when retrieved
|
||||||
self.assertRedirects(response, 'http://testserver/test_client/permanent_redirect_view/', target_status_code=301)
|
self.assertRedirects(response, 'http://testserver/test_client/permanent_redirect_view/', target_status_code=301)
|
||||||
|
|
||||||
|
def test_follow_redirect(self):
|
||||||
|
"A URL that redirects can be followed to termination."
|
||||||
|
response = self.client.get('/test_client/double_redirect_view/', follow=True)
|
||||||
|
self.assertRedirects(response, 'http://testserver/test_client/get_view/', status_code=302, target_status_code=200)
|
||||||
|
self.assertEquals(len(response.redirect_chain), 2)
|
||||||
|
|
||||||
def test_notfound_response(self):
|
def test_notfound_response(self):
|
||||||
"GET a URL that responds as '404:Not Found'"
|
"GET a URL that responds as '404:Not Found'"
|
||||||
response = self.client.get('/test_client/bad_view/')
|
response = self.client.get('/test_client/bad_view/')
|
||||||
|
|
|
@ -148,6 +148,107 @@ class AssertRedirectsTests(TestCase):
|
||||||
except AssertionError, e:
|
except AssertionError, e:
|
||||||
self.assertEquals(str(e), "Couldn't retrieve redirection page '/test_client/permanent_redirect_view/': response code was 301 (expected 200)")
|
self.assertEquals(str(e), "Couldn't retrieve redirection page '/test_client/permanent_redirect_view/': response code was 301 (expected 200)")
|
||||||
|
|
||||||
|
def test_redirect_chain(self):
|
||||||
|
"You can follow a redirect chain of multiple redirects"
|
||||||
|
response = self.client.get('/test_client_regress/redirects/further/more/', {}, follow=True)
|
||||||
|
self.assertRedirects(response, '/test_client_regress/no_template_view/',
|
||||||
|
status_code=301, target_status_code=200)
|
||||||
|
|
||||||
|
self.assertEquals(len(response.redirect_chain), 1)
|
||||||
|
self.assertEquals(response.redirect_chain[0], ('http://testserver/test_client_regress/no_template_view/', 301))
|
||||||
|
|
||||||
|
def test_multiple_redirect_chain(self):
|
||||||
|
"You can follow a redirect chain of multiple redirects"
|
||||||
|
response = self.client.get('/test_client_regress/redirects/', {}, follow=True)
|
||||||
|
self.assertRedirects(response, '/test_client_regress/no_template_view/',
|
||||||
|
status_code=301, target_status_code=200)
|
||||||
|
|
||||||
|
self.assertEquals(len(response.redirect_chain), 3)
|
||||||
|
self.assertEquals(response.redirect_chain[0], ('http://testserver/test_client_regress/redirects/further/', 301))
|
||||||
|
self.assertEquals(response.redirect_chain[1], ('http://testserver/test_client_regress/redirects/further/more/', 301))
|
||||||
|
self.assertEquals(response.redirect_chain[2], ('http://testserver/test_client_regress/no_template_view/', 301))
|
||||||
|
|
||||||
|
def test_redirect_chain_to_non_existent(self):
|
||||||
|
"You can follow a chain to a non-existent view"
|
||||||
|
response = self.client.get('/test_client_regress/redirect_to_non_existent_view2/', {}, follow=True)
|
||||||
|
self.assertRedirects(response, '/test_client_regress/non_existent_view/',
|
||||||
|
status_code=301, target_status_code=404)
|
||||||
|
|
||||||
|
def test_redirect_chain_to_self(self):
|
||||||
|
"Redirections to self are caught and escaped"
|
||||||
|
response = self.client.get('/test_client_regress/redirect_to_self/', {}, follow=True)
|
||||||
|
# The chain of redirects stops once the cycle is detected.
|
||||||
|
self.assertRedirects(response, '/test_client_regress/redirect_to_self/',
|
||||||
|
status_code=301, target_status_code=301)
|
||||||
|
self.assertEquals(len(response.redirect_chain), 2)
|
||||||
|
|
||||||
|
def test_circular_redirect(self):
|
||||||
|
"Circular redirect chains are caught and escaped"
|
||||||
|
response = self.client.get('/test_client_regress/circular_redirect_1/', {}, follow=True)
|
||||||
|
# The chain of redirects will get back to the starting point, but stop there.
|
||||||
|
self.assertRedirects(response, '/test_client_regress/circular_redirect_2/',
|
||||||
|
status_code=301, target_status_code=301)
|
||||||
|
self.assertEquals(len(response.redirect_chain), 4)
|
||||||
|
|
||||||
|
def test_redirect_chain_post(self):
|
||||||
|
"A redirect chain will be followed from an initial POST post"
|
||||||
|
response = self.client.post('/test_client_regress/redirects/',
|
||||||
|
{'nothing': 'to_send'}, follow=True)
|
||||||
|
self.assertRedirects(response,
|
||||||
|
'/test_client_regress/no_template_view/', 301, 200)
|
||||||
|
self.assertEquals(len(response.redirect_chain), 3)
|
||||||
|
|
||||||
|
def test_redirect_chain_head(self):
|
||||||
|
"A redirect chain will be followed from an initial HEAD request"
|
||||||
|
response = self.client.head('/test_client_regress/redirects/',
|
||||||
|
{'nothing': 'to_send'}, follow=True)
|
||||||
|
self.assertRedirects(response,
|
||||||
|
'/test_client_regress/no_template_view/', 301, 200)
|
||||||
|
self.assertEquals(len(response.redirect_chain), 3)
|
||||||
|
|
||||||
|
def test_redirect_chain_options(self):
|
||||||
|
"A redirect chain will be followed from an initial OPTIONS request"
|
||||||
|
response = self.client.options('/test_client_regress/redirects/',
|
||||||
|
{'nothing': 'to_send'}, follow=True)
|
||||||
|
self.assertRedirects(response,
|
||||||
|
'/test_client_regress/no_template_view/', 301, 200)
|
||||||
|
self.assertEquals(len(response.redirect_chain), 3)
|
||||||
|
|
||||||
|
def test_redirect_chain_put(self):
|
||||||
|
"A redirect chain will be followed from an initial PUT request"
|
||||||
|
response = self.client.put('/test_client_regress/redirects/',
|
||||||
|
{'nothing': 'to_send'}, follow=True)
|
||||||
|
self.assertRedirects(response,
|
||||||
|
'/test_client_regress/no_template_view/', 301, 200)
|
||||||
|
self.assertEquals(len(response.redirect_chain), 3)
|
||||||
|
|
||||||
|
def test_redirect_chain_delete(self):
|
||||||
|
"A redirect chain will be followed from an initial DELETE request"
|
||||||
|
response = self.client.delete('/test_client_regress/redirects/',
|
||||||
|
{'nothing': 'to_send'}, follow=True)
|
||||||
|
self.assertRedirects(response,
|
||||||
|
'/test_client_regress/no_template_view/', 301, 200)
|
||||||
|
self.assertEquals(len(response.redirect_chain), 3)
|
||||||
|
|
||||||
|
def test_redirect_chain_on_non_redirect_page(self):
|
||||||
|
"An assertion is raised if the original page couldn't be retrieved as expected"
|
||||||
|
# This page will redirect with code 301, not 302
|
||||||
|
response = self.client.get('/test_client/get_view/', follow=True)
|
||||||
|
try:
|
||||||
|
self.assertRedirects(response, '/test_client/get_view/')
|
||||||
|
except AssertionError, e:
|
||||||
|
self.assertEquals(str(e), "Response didn't redirect as expected: Response code was 200 (expected 302)")
|
||||||
|
|
||||||
|
def test_redirect_on_non_redirect_page(self):
|
||||||
|
"An assertion is raised if the original page couldn't be retrieved as expected"
|
||||||
|
# This page will redirect with code 301, not 302
|
||||||
|
response = self.client.get('/test_client/get_view/')
|
||||||
|
try:
|
||||||
|
self.assertRedirects(response, '/test_client/get_view/')
|
||||||
|
except AssertionError, e:
|
||||||
|
self.assertEquals(str(e), "Response didn't redirect as expected: Response code was 200 (expected 302)")
|
||||||
|
|
||||||
|
|
||||||
class AssertFormErrorTests(TestCase):
|
class AssertFormErrorTests(TestCase):
|
||||||
def test_unknown_form(self):
|
def test_unknown_form(self):
|
||||||
"An assertion is raised if the form name is unknown"
|
"An assertion is raised if the form name is unknown"
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from django.conf.urls.defaults import *
|
from django.conf.urls.defaults import *
|
||||||
|
from django.views.generic.simple import redirect_to
|
||||||
import views
|
import views
|
||||||
|
|
||||||
urlpatterns = patterns('',
|
urlpatterns = patterns('',
|
||||||
|
@ -8,6 +9,15 @@ urlpatterns = patterns('',
|
||||||
(r'^request_data/$', views.request_data),
|
(r'^request_data/$', views.request_data),
|
||||||
url(r'^arg_view/(?P<name>.+)/$', views.view_with_argument, name='arg_view'),
|
url(r'^arg_view/(?P<name>.+)/$', views.view_with_argument, name='arg_view'),
|
||||||
(r'^login_protected_redirect_view/$', views.login_protected_redirect_view),
|
(r'^login_protected_redirect_view/$', views.login_protected_redirect_view),
|
||||||
|
(r'^redirects/$', redirect_to, {'url': '/test_client_regress/redirects/further/'}),
|
||||||
|
(r'^redirects/further/$', redirect_to, {'url': '/test_client_regress/redirects/further/more/'}),
|
||||||
|
(r'^redirects/further/more/$', redirect_to, {'url': '/test_client_regress/no_template_view/'}),
|
||||||
|
(r'^redirect_to_non_existent_view/$', redirect_to, {'url': '/test_client_regress/non_existent_view/'}),
|
||||||
|
(r'^redirect_to_non_existent_view2/$', redirect_to, {'url': '/test_client_regress/redirect_to_non_existent_view/'}),
|
||||||
|
(r'^redirect_to_self/$', redirect_to, {'url': '/test_client_regress/redirect_to_self/'}),
|
||||||
|
(r'^circular_redirect_1/$', redirect_to, {'url': '/test_client_regress/circular_redirect_2/'}),
|
||||||
|
(r'^circular_redirect_2/$', redirect_to, {'url': '/test_client_regress/circular_redirect_3/'}),
|
||||||
|
(r'^circular_redirect_3/$', redirect_to, {'url': '/test_client_regress/circular_redirect_1/'}),
|
||||||
(r'^set_session/$', views.set_session_view),
|
(r'^set_session/$', views.set_session_view),
|
||||||
(r'^check_session/$', views.check_session_view),
|
(r'^check_session/$', views.check_session_view),
|
||||||
(r'^request_methods/$', views.request_methods_view),
|
(r'^request_methods/$', views.request_methods_view),
|
||||||
|
|
|
@ -58,4 +58,4 @@ def check_session_view(request):
|
||||||
|
|
||||||
def request_methods_view(request):
|
def request_methods_view(request):
|
||||||
"A view that responds with the request method"
|
"A view that responds with the request method"
|
||||||
return HttpResponse('request method: %s' % request.method)
|
return HttpResponse('request method: %s' % request.method)
|
||||||
|
|
Loading…
Reference in New Issue