summaryrefslogtreecommitdiff
path: root/django/test
diff options
context:
space:
mode:
Diffstat (limited to 'django/test')
-rw-r--r--django/test/__init__.py6
-rw-r--r--django/test/_doctest.py (renamed from django/test/doctest.py)5
-rw-r--r--django/test/client.py127
-rw-r--r--django/test/simple.py21
-rw-r--r--django/test/testcases.py123
-rw-r--r--django/test/utils.py77
6 files changed, 298 insertions, 61 deletions
diff --git a/django/test/__init__.py b/django/test/__init__.py
index e69de29bb2..554e72bad3 100644
--- a/django/test/__init__.py
+++ b/django/test/__init__.py
@@ -0,0 +1,6 @@
+"""
+Django Unit Test and Doctest framework.
+"""
+
+from django.test.client import Client
+from django.test.testcases import TestCase
diff --git a/django/test/doctest.py b/django/test/_doctest.py
index 3b364f0a75..8777a2cbba 100644
--- a/django/test/doctest.py
+++ b/django/test/_doctest.py
@@ -1,3 +1,8 @@
+# This is a slightly modified version of the doctest.py that shipped with Python 2.4
+# It incorporates changes that have been submitted the the Python ticket tracker
+# as ticket #1521051. These changes allow for a DoctestRunner and Doctest base
+# class to be specified when constructing a DoctestSuite.
+
# Module doctest.
# Released to the public domain 16-Jan-2001, by Tim Peters (tim@python.org).
# Major enhancements and refactoring by:
diff --git a/django/test/client.py b/django/test/client.py
index 6e0b443f83..e4fd54c23b 100644
--- a/django/test/client.py
+++ b/django/test/client.py
@@ -1,11 +1,22 @@
+import datetime
+import sys
from cStringIO import StringIO
+from urlparse import urlparse
+from django.conf import settings
+from django.contrib.auth import authenticate, login
+from django.contrib.sessions.models import Session
+from django.contrib.sessions.middleware import SessionWrapper
from django.core.handlers.base import BaseHandler
from django.core.handlers.wsgi import WSGIRequest
+from django.core.signals import got_request_exception
from django.dispatch import dispatcher
-from django.http import urlencode, SimpleCookie
+from django.http import urlencode, SimpleCookie, HttpRequest
from django.test import signals
from django.utils.functional import curry
+BOUNDARY = 'BoUnDaRyStRiNg'
+MULTIPART_CONTENT = 'multipart/form-data; boundary=%s' % BOUNDARY
+
class ClientHandler(BaseHandler):
"""
A HTTP Handler that can be used for testing purposes.
@@ -54,14 +65,19 @@ def encode_multipart(boundary, data):
if isinstance(value, file):
lines.extend([
'--' + boundary,
- 'Content-Disposition: form-data; name="%s"' % key,
- '',
- '--' + boundary,
- 'Content-Disposition: form-data; name="%s_file"; filename="%s"' % (key, value.name),
+ 'Content-Disposition: form-data; name="%s"; filename="%s"' % (key, value.name),
'Content-Type: application/octet-stream',
'',
value.read()
])
+ elif hasattr(value, '__iter__'):
+ for item in value:
+ lines.extend([
+ '--' + boundary,
+ 'Content-Disposition: form-data; name="%s"' % key,
+ '',
+ str(item)
+ ])
else:
lines.extend([
'--' + boundary,
@@ -97,8 +113,25 @@ class Client:
def __init__(self, **defaults):
self.handler = ClientHandler()
self.defaults = defaults
- self.cookie = SimpleCookie()
+ self.cookies = SimpleCookie()
+ self.exc_info = None
+
+ def store_exc_info(self, *args, **kwargs):
+ """
+ Utility method that can be used to store exceptions when they are
+ generated by a view.
+ """
+ self.exc_info = sys.exc_info()
+ def _session(self):
+ "Obtain the current session variables"
+ if 'django.contrib.sessions' in settings.INSTALLED_APPS:
+ cookie = self.cookies.get(settings.SESSION_COOKIE_NAME, None)
+ if cookie:
+ return SessionWrapper(cookie.value)
+ return {}
+ session = property(_session)
+
def request(self, **request):
"""
The master request method. Composes the environment dictionary
@@ -108,7 +141,7 @@ class Client:
"""
environ = {
- 'HTTP_COOKIE': self.cookie,
+ 'HTTP_COOKIE': self.cookies,
'PATH_INFO': '/',
'QUERY_STRING': '',
'REQUEST_METHOD': 'GET',
@@ -126,6 +159,9 @@ class Client:
on_template_render = curry(store_rendered_templates, data)
dispatcher.connect(on_template_render, signal=signals.template_rendered)
+ # Capture exceptions created by the handler
+ dispatcher.connect(self.store_exc_info, signal=got_request_exception)
+
response = self.handler(environ)
# Add any rendered template detail to the response
@@ -140,8 +176,13 @@ class Client:
else:
setattr(response, detail, None)
+ # Look for a signalled exception and reraise it
+ if self.exc_info:
+ raise self.exc_info[1], None, self.exc_info[2]
+
+ # Update persistent cookie data
if response.cookies:
- self.cookie.update(response.cookies)
+ self.cookies.update(response.cookies)
return response
@@ -158,59 +199,53 @@ class Client:
return self.request(**r)
- def post(self, path, data={}, **extra):
+ def post(self, path, data={}, content_type=MULTIPART_CONTENT, **extra):
"Request a response from the server using POST."
- BOUNDARY = 'BoUnDaRyStRiNg'
+ if content_type is MULTIPART_CONTENT:
+ post_data = encode_multipart(BOUNDARY, data)
+ else:
+ post_data = data
- encoded = encode_multipart(BOUNDARY, data)
- stream = StringIO(encoded)
r = {
- 'CONTENT_LENGTH': len(encoded),
- 'CONTENT_TYPE': 'multipart/form-data; boundary=%s' % BOUNDARY,
+ 'CONTENT_LENGTH': len(post_data),
+ 'CONTENT_TYPE': content_type,
'PATH_INFO': path,
'REQUEST_METHOD': 'POST',
- 'wsgi.input': stream,
+ 'wsgi.input': StringIO(post_data),
}
r.update(extra)
return self.request(**r)
- def login(self, path, username, password, **extra):
- """
- A specialized sequence of GET and POST to log into a view that
- is protected by a @login_required access decorator.
-
- path should be the URL of the page that is login protected.
+ def login(self, **credentials):
+ """Set the Client to appear as if it has sucessfully logged into a site.
- Returns the response from GETting the requested URL after
- login is complete. Returns False if login process failed.
+ Returns True if login is possible; False if the provided credentials
+ are incorrect, or if the Sessions framework is not available.
"""
- # First, GET the page that is login protected.
- # This page will redirect to the login page.
- response = self.get(path)
- if response.status_code != 302:
- return False
+ user = authenticate(**credentials)
+ if user and 'django.contrib.sessions' in settings.INSTALLED_APPS:
+ obj = Session.objects.get_new_session_object()
- login_path, data = response['Location'].split('?')
- next = data.split('=')[1]
+ # Create a fake request to store login details
+ request = HttpRequest()
+ request.session = SessionWrapper(obj.session_key)
+ login(request, user)
- # Second, GET the login page; required to set up cookies
- response = self.get(login_path, **extra)
- if response.status_code != 200:
- return False
+ # Set the cookie to represent the session
+ self.cookies[settings.SESSION_COOKIE_NAME] = obj.session_key
+ self.cookies[settings.SESSION_COOKIE_NAME]['max-age'] = None
+ self.cookies[settings.SESSION_COOKIE_NAME]['path'] = '/'
+ self.cookies[settings.SESSION_COOKIE_NAME]['domain'] = settings.SESSION_COOKIE_DOMAIN
+ self.cookies[settings.SESSION_COOKIE_NAME]['secure'] = settings.SESSION_COOKIE_SECURE or None
+ self.cookies[settings.SESSION_COOKIE_NAME]['expires'] = None
- # Last, POST the login data.
- form_data = {
- 'username': username,
- 'password': password,
- 'next' : next,
- }
- response = self.post(login_path, data=form_data, **extra)
+ # Set the session values
+ Session.objects.save(obj.session_key, request.session._session,
+ datetime.datetime.now() + datetime.timedelta(seconds=settings.SESSION_COOKIE_AGE))
- # Login page should 302 redirect to the originally requested page
- if response.status_code != 302 or response['Location'] != path:
+ return True
+ else:
return False
-
- # Since we are logged in, request the actual page again
- return self.get(path)
+
diff --git a/django/test/simple.py b/django/test/simple.py
index 88e6b49925..5f7f86f220 100644
--- a/django/test/simple.py
+++ b/django/test/simple.py
@@ -1,6 +1,6 @@
-import unittest, doctest
+import unittest
from django.conf import settings
-from django.core import management
+from django.test import _doctest as doctest
from django.test.utils import setup_test_environment, teardown_test_environment
from django.test.utils import create_test_db, destroy_test_db
from django.test.testcases import OutputChecker, DocTestRunner
@@ -50,9 +50,12 @@ def build_suite(app_module):
pass
else:
# The module exists, so there must be an import error in the
- # test module itself. We don't need the module; close the file
- # handle returned by find_module.
- mod[0].close()
+ # test module itself. We don't need the module; so if the
+ # module was a single file module (i.e., tests.py), close the file
+ # handle returned by find_module. Otherwise, the test module
+ # is a directory, and there is nothing to close.
+ if mod[0]:
+ mod[0].close()
raise
return suite
@@ -64,6 +67,8 @@ def run_tests(module_list, verbosity=1, extra_tests=[]):
looking for doctests and unittests in models.py or tests.py within
the module. A list of 'extra' tests may also be provided; these tests
will be added to the test suite.
+
+ Returns the number of tests that failed.
"""
setup_test_environment()
@@ -78,8 +83,10 @@ def run_tests(module_list, verbosity=1, extra_tests=[]):
old_name = settings.DATABASE_NAME
create_test_db(verbosity)
- management.syncdb(verbosity, interactive=False)
- unittest.TextTestRunner(verbosity=verbosity).run(suite)
+ result = unittest.TextTestRunner(verbosity=verbosity).run(suite)
destroy_test_db(old_name, verbosity)
teardown_test_environment()
+
+ return len(result.failures) + len(result.errors)
+ \ No newline at end of file
diff --git a/django/test/testcases.py b/django/test/testcases.py
index 1cfef6f19e..2bc1b5a5f8 100644
--- a/django/test/testcases.py
+++ b/django/test/testcases.py
@@ -1,6 +1,11 @@
-import re, doctest, unittest
+import re, unittest
+from urlparse import urlparse
from django.db import transaction
-
+from django.core import management, mail
+from django.db.models import get_apps
+from django.test import _doctest as doctest
+from django.test.client import Client
+
normalize_long_ints = lambda s: re.sub(r'(?<![\w])(\d+)L(?![\w])', '\\1', s)
class OutputChecker(doctest.OutputChecker):
@@ -28,3 +33,117 @@ class DocTestRunner(doctest.DocTestRunner):
from django.db import transaction
transaction.rollback_unless_managed()
+class TestCase(unittest.TestCase):
+ def _pre_setup(self):
+ """Perform any pre-test setup. This includes:
+
+ * If the Test Case class has a 'fixtures' member, clearing the
+ database and installing the named fixtures at the start of each test.
+ * Clearing the mail test outbox.
+
+ """
+ management.flush(verbosity=0, interactive=False)
+ if hasattr(self, 'fixtures'):
+ management.load_data(self.fixtures, verbosity=0)
+ mail.outbox = []
+
+ def __call__(self, result=None):
+ """
+ Wrapper around default __call__ method to perform common Django test
+ set up. This means that user-defined Test Cases aren't required to
+ include a call to super().setUp().
+ """
+ self.client = Client()
+ self._pre_setup()
+ super(TestCase, self).__call__(result)
+
+ def assertRedirects(self, response, expected_path, status_code=302, target_status_code=200):
+ """Assert that a response redirected to a specific URL, and that the
+ redirect URL can be loaded.
+
+ """
+ self.assertEqual(response.status_code, status_code,
+ "Response didn't redirect as expected: Reponse code was %d (expected %d)" %
+ (response.status_code, status_code))
+ scheme, netloc, path, params, query, fragment = urlparse(response['Location'])
+ self.assertEqual(path, expected_path,
+ "Response redirected to '%s', expected '%s'" % (path, expected_path))
+ redirect_response = self.client.get(path)
+ 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=1, status_code=200):
+ """Assert that a response indicates that a page was retreived successfully,
+ (i.e., the HTTP status code was as expected), and that ``text`` occurs ``count``
+ times in the content of the response.
+
+ """
+ self.assertEqual(response.status_code, status_code,
+ "Couldn't retrieve page: Response code was %d (expected %d)'" %
+ (response.status_code, status_code))
+ real_count = response.content.count(text)
+ self.assertEqual(real_count, count,
+ "Found %d instances of '%s' in response (expected %d)" % (real_count, text, count))
+
+ def assertFormError(self, response, form, field, errors):
+ "Assert that a form used to render the response has a specific field error"
+ if not response.context:
+ self.fail('Response did not use any contexts to render the response')
+
+ # If there is a single context, put it into a list to simplify processing
+ if not isinstance(response.context, list):
+ contexts = [response.context]
+ else:
+ contexts = response.context
+
+ # If a single error string is provided, make it a list to simplify processing
+ if not isinstance(errors, list):
+ errors = [errors]
+
+ # Search all contexts for the error.
+ found_form = False
+ for i,context in enumerate(contexts):
+ if form in context:
+ found_form = True
+ for err in errors:
+ if field:
+ if field in context[form].errors:
+ self.failUnless(err in context[form].errors[field],
+ "The field '%s' on form '%s' in context %d does not contain the error '%s' (actual errors: %s)" %
+ (field, form, i, err, list(context[form].errors[field])))
+ elif field in context[form].fields:
+ self.fail("The field '%s' on form '%s' in context %d contains no errors" %
+ (field, form, i))
+ else:
+ self.fail("The form '%s' in context %d does not contain the field '%s'" % (form, i, field))
+ else:
+ self.failUnless(err in context[form].non_field_errors(),
+ "The form '%s' in context %d does not contain the non-field error '%s' (actual errors: %s)" %
+ (form, i, err, list(context[form].non_field_errors())))
+ if not found_form:
+ self.fail("The form '%s' was not used to render the response" % form)
+
+ def assertTemplateUsed(self, response, template_name):
+ "Assert that the template with the provided name was used in rendering the response"
+ if isinstance(response.template, list):
+ template_names = [t.name for t in response.template]
+ self.failUnless(template_name in template_names,
+ "Template '%s' was not one of the templates used to render the response. Templates used: %s" %
+ (template_name, template_names))
+ elif response.template:
+ self.assertEqual(template_name, response.template.name,
+ "Template '%s' was not used to render the response. Actual template was '%s'" %
+ (template_name, response.template.name))
+ else:
+ self.fail('No templates used to render the response')
+
+ def assertTemplateNotUsed(self, response, template_name):
+ "Assert that the template with the provided name was NOT used in rendering the response"
+ if isinstance(response.template, list):
+ self.failIf(template_name in [t.name for t in response.template],
+ "Template '%s' was used unexpectedly in rendering the response" % template_name)
+ elif response.template:
+ self.assertNotEqual(template_name, response.template.name,
+ "Template '%s' was used unexpectedly in rendering the response" % template_name)
+
diff --git a/django/test/utils.py b/django/test/utils.py
index 039a6dd7a2..303a223183 100644
--- a/django/test/utils.py
+++ b/django/test/utils.py
@@ -1,6 +1,7 @@
import sys, time
from django.conf import settings
from django.db import connection, transaction, backend
+from django.core import management, mail
from django.dispatch import dispatcher
from django.test import signals
from django.template import Template
@@ -10,30 +11,72 @@ from django.template import Template
TEST_DATABASE_PREFIX = 'test_'
def instrumented_test_render(self, context):
- """An instrumented Template render method, providing a signal
- that can be intercepted by the test system Client
-
+ """
+ An instrumented Template render method, providing a signal that can be
+ intercepted by the test system Client.
"""
dispatcher.send(signal=signals.template_rendered, sender=self, template=self, context=context)
return self.nodelist.render(context)
+
+def instrumented_test_iter_render(self, context):
+ """
+ An instrumented Template iter_render method, providing a signal that can be
+ intercepted by the test system Client.
+ """
+ for chunk in self.nodelist.iter_render(context):
+ yield chunk
+ dispatcher.send(signal=signals.template_rendered, sender=self, template=self, context=context)
+class TestSMTPConnection(object):
+ """A substitute SMTP connection for use during test sessions.
+ The test connection stores email messages in a dummy outbox,
+ rather than sending them out on the wire.
+
+ """
+ def __init__(*args, **kwargs):
+ pass
+ def open(self):
+ "Mock the SMTPConnection open() interface"
+ pass
+ def close(self):
+ "Mock the SMTPConnection close() interface"
+ pass
+ def send_messages(self, messages):
+ "Redirect messages to the dummy outbox"
+ mail.outbox.extend(messages)
+
def setup_test_environment():
"""Perform any global pre-test setup. This involves:
- Installing the instrumented test renderer
+ - Diverting the email sending functions to a test buffer
"""
Template.original_render = Template.render
+ Template.original_iter_render = Template.iter_render
Template.render = instrumented_test_render
+ Template.iter_render = instrumented_test_render
+
+ mail.original_SMTPConnection = mail.SMTPConnection
+ mail.SMTPConnection = TestSMTPConnection
+
+ mail.outbox = []
def teardown_test_environment():
"""Perform any global post-test teardown. This involves:
- Restoring the original test renderer
+ - Restoring the email sending functions
"""
Template.render = Template.original_render
- del Template.original_render
+ Template.iter_render = Template.original_iter_render
+ del Template.original_render, Template.original_iter_render
+
+ mail.SMTPConnection = mail.original_SMTPConnection
+ del mail.original_SMTPConnection
+
+ del mail.outbox
def _set_autocommit(connection):
"Make sure a connection is in autocommit mode."
@@ -42,6 +85,20 @@ def _set_autocommit(connection):
elif hasattr(connection.connection, "set_isolation_level"):
connection.connection.set_isolation_level(0)
+def get_mysql_create_suffix():
+ suffix = []
+ if settings.TEST_DATABASE_CHARSET:
+ suffix.append('CHARACTER SET %s' % settings.TEST_DATABASE_CHARSET)
+ if settings.TEST_DATABASE_COLLATION:
+ suffix.append('COLLATE %s' % settings.TEST_DATABASE_COLLATION)
+ return ' '.join(suffix)
+
+def get_postgresql_create_suffix():
+ assert settings.TEST_DATABASE_COLLATION is None, "PostgreSQL does not support collation setting at database creation time."
+ if settings.TEST_DATABASE_CHARSET:
+ return "WITH ENCODING '%s'" % settings.TEST_DATABASE_CHARSET
+ return ''
+
def create_test_db(verbosity=1, autoclobber=False):
if verbosity >= 1:
print "Creating test database..."
@@ -50,6 +107,12 @@ def create_test_db(verbosity=1, autoclobber=False):
if settings.DATABASE_ENGINE == "sqlite3":
TEST_DATABASE_NAME = ":memory:"
else:
+ suffix = {
+ 'postgresql': get_postgresql_create_suffix,
+ 'postgresql_psycopg2': get_postgresql_create_suffix,
+ 'mysql': get_mysql_create_suffix,
+ 'mysql_old': get_mysql_create_suffix,
+ }.get(settings.DATABASE_ENGINE, lambda: '')()
if settings.TEST_DATABASE_NAME:
TEST_DATABASE_NAME = settings.TEST_DATABASE_NAME
else:
@@ -61,7 +124,7 @@ def create_test_db(verbosity=1, autoclobber=False):
cursor = connection.cursor()
_set_autocommit(connection)
try:
- cursor.execute("CREATE DATABASE %s" % backend.quote_name(TEST_DATABASE_NAME))
+ cursor.execute("CREATE DATABASE %s %s" % (backend.quote_name(TEST_DATABASE_NAME), suffix))
except Exception, e:
sys.stderr.write("Got an error creating the test database: %s\n" % e)
if not autoclobber:
@@ -73,7 +136,7 @@ def create_test_db(verbosity=1, autoclobber=False):
cursor.execute("DROP DATABASE %s" % backend.quote_name(TEST_DATABASE_NAME))
if verbosity >= 1:
print "Creating test database..."
- cursor.execute("CREATE DATABASE %s" % backend.quote_name(TEST_DATABASE_NAME))
+ cursor.execute("CREATE DATABASE %s %s" % (backend.quote_name(TEST_DATABASE_NAME), suffix))
except Exception, e:
sys.stderr.write("Got an error recreating the test database: %s\n" % e)
sys.exit(2)
@@ -84,6 +147,8 @@ def create_test_db(verbosity=1, autoclobber=False):
connection.close()
settings.DATABASE_NAME = TEST_DATABASE_NAME
+ management.syncdb(verbosity, interactive=False)
+
# Get a cursor (even though we don't need one yet). This has
# the side effect of initializing the test database.
cursor = connection.cursor()