diff options
Diffstat (limited to 'django/test')
| -rw-r--r-- | django/test/__init__.py | 6 | ||||
| -rw-r--r-- | django/test/_doctest.py (renamed from django/test/doctest.py) | 5 | ||||
| -rw-r--r-- | django/test/client.py | 127 | ||||
| -rw-r--r-- | django/test/simple.py | 21 | ||||
| -rw-r--r-- | django/test/testcases.py | 123 | ||||
| -rw-r--r-- | django/test/utils.py | 77 |
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() |
