summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBoulder Sprinters <boulder-sprinters@djangoproject.com>2007-05-07 15:50:55 +0000
committerBoulder Sprinters <boulder-sprinters@djangoproject.com>2007-05-07 15:50:55 +0000
commita275d3da8ed8cea8c2c92fc15151f43fb56b42ce (patch)
tree9f9a7c291956a17587b898772c5949b7a44864cd
parent0f22c6a7c8a089e2381a058b4c472cf92699950e (diff)
downloaddjango-a275d3da8ed8cea8c2c92fc15151f43fb56b42ce.tar.gz
boulder-oracle-sprint: Merged to [5156]
git-svn-id: http://code.djangoproject.com/svn/django/branches/boulder-oracle-sprint@5157 bcc190cf-cafb-0310-a4f2-bffc1f526a37
-rw-r--r--django/db/models/query.py2
-rw-r--r--django/test/client.py88
-rw-r--r--django/test/testcases.py90
-rw-r--r--docs/email.txt78
-rw-r--r--docs/testing.txt192
-rw-r--r--tests/modeltests/custom_columns/models.py2
-rw-r--r--tests/modeltests/lookup/models.py4
-rw-r--r--tests/modeltests/many_to_one/models.py4
-rw-r--r--tests/modeltests/reverse_lookup/models.py2
-rw-r--r--tests/modeltests/test_client/models.py94
-rw-r--r--tests/modeltests/test_client/urls.py2
-rw-r--r--tests/modeltests/test_client/views.py24
-rw-r--r--tests/regressiontests/null_queries/models.py2
-rw-r--r--tests/templates/base.html8
-rw-r--r--tests/templates/form_view.html15
-rw-r--r--tests/templates/login.html10
16 files changed, 450 insertions, 167 deletions
diff --git a/django/db/models/query.py b/django/db/models/query.py
index 93dcdd1776..d31ccf003e 100644
--- a/django/db/models/query.py
+++ b/django/db/models/query.py
@@ -998,7 +998,7 @@ def lookup_inner(path, lookup_type, value, opts, table, column):
field_choices(current_opts.get_all_related_many_to_many_objects(), True) + \
field_choices(current_opts.get_all_related_objects(), True) + \
field_choices(current_opts.fields, False)
- raise TypeError, "Cannot resolve keyword '%s' into field, choices are: %s" % (name, ", ".join(choices))
+ raise TypeError, "Cannot resolve keyword '%s' into field. Choices are: %s" % (name, ", ".join(choices))
# Check whether an intermediate join is required between current_table
# and new_table.
diff --git a/django/test/client.py b/django/test/client.py
index 95d3b85922..c3110f02ec 100644
--- a/django/test/client.py
+++ b/django/test/client.py
@@ -1,12 +1,16 @@
+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
@@ -113,7 +117,6 @@ class Client:
self.handler = ClientHandler()
self.defaults = defaults
self.cookies = SimpleCookie()
- self.session = {}
self.exc_info = None
def store_exc_info(self, *args, **kwargs):
@@ -123,6 +126,15 @@ class Client:
"""
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
@@ -171,16 +183,10 @@ class Client:
if self.exc_info:
raise self.exc_info[1], None, self.exc_info[2]
- # Update persistent cookie and session data
+ # Update persistent cookie data
if response.cookies:
self.cookies.update(response.cookies)
- if 'django.contrib.sessions' in settings.INSTALLED_APPS:
- from django.contrib.sessions.middleware import SessionWrapper
- cookie = self.cookies.get(settings.SESSION_COOKIE_NAME, None)
- if cookie:
- self.session = SessionWrapper(cookie.value)
-
return response
def get(self, path, data={}, **extra):
@@ -215,42 +221,34 @@ class Client:
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
-
- _, _, login_path, _, data, _= urlparse(response['Location'])
- next = data.split('=')[1]
-
- # Second, GET the login page; required to set up cookies
- response = self.get(login_path, **extra)
- if response.status_code != 200:
- return False
-
- # Last, POST the login data.
- form_data = {
- 'username': username,
- 'password': password,
- 'next' : next,
- }
- response = self.post(login_path, data=form_data, **extra)
-
- # Login page should 302 redirect to the originally requested page
- if (response.status_code != 302 or
- urlparse(response['Location'])[2] != path):
+ user = authenticate(**credentials)
+ if user and 'django.contrib.sessions' in settings.INSTALLED_APPS:
+ obj = Session.objects.get_new_session_object()
+
+ # Create a fake request to store login details
+ request = HttpRequest()
+ request.session = SessionWrapper(obj.session_key)
+ login(request, user)
+
+ # 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
+
+ # Set the session values
+ Session.objects.save(obj.session_key, request.session._session,
+ datetime.datetime.now() + datetime.timedelta(seconds=settings.SESSION_COOKIE_AGE))
+
+ return True
+ else:
return False
-
- # Since we are logged in, request the actual page again
- return self.get(path)
+ \ No newline at end of file
diff --git a/django/test/testcases.py b/django/test/testcases.py
index 2bfb9a733a..80f55b20d3 100644
--- a/django/test/testcases.py
+++ b/django/test/testcases.py
@@ -1,8 +1,10 @@
import re, doctest, unittest
+from urlparse import urlparse
from django.db import transaction
from django.core import management
from django.db.models import get_apps
-
+from django.test.client import Client
+
normalize_long_ints = lambda s: re.sub(r'(?<![\w])(\d+)L(?![\w])', '\\1', s)
class OutputChecker(doctest.OutputChecker):
@@ -46,5 +48,91 @@ class TestCase(unittest.TestCase):
super().
"""
+ self.client = Client()
self.install_fixtures()
super(TestCase, self).run(result)
+
+ def assertRedirects(self, response, expected_path):
+ """Assert that a response redirected to a specific URL, and that the
+ redirect URL can be loaded.
+
+ """
+ self.assertEqual(response.status_code, 302,
+ "Response didn't redirect: Reponse code was %d" % response.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, 200,
+ "Couldn't retrieve redirection page '%s'" % path)
+
+ def assertContains(self, response, text, count=1):
+ """Assert that a response indicates that a page was retreived successfully,
+ (i.e., the HTTP status code was 200), and that ``text`` occurs ``count``
+ times in the content of the response.
+
+ """
+ self.assertEqual(response.status_code, 200,
+ "Couldn't retrieve page'")
+ real_count = response.content.count(text)
+ self.assertEqual(real_count, count,
+ "Could only find %d of %d instances of '%s' in response" % (real_count, count, text))
+
+ 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
+ try:
+ for err in errors:
+ if field:
+ self.assertTrue(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])))
+ else:
+ self.assertTrue(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())))
+ except KeyError:
+ self.fail("The form '%s' in context %d does not contain the field '%s'" % (form, i, field))
+ 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.assertTrue(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.assertFalse(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)
+ \ No newline at end of file
diff --git a/docs/email.txt b/docs/email.txt
index 2793ee8ae3..66948e5294 100644
--- a/docs/email.txt
+++ b/docs/email.txt
@@ -20,14 +20,14 @@ In two lines::
send_mail('Subject here', 'Here is the message.', 'from@example.com',
['to@example.com'], fail_silently=False)
-Mail will be sent using the SMTP host and port specified in the `EMAIL_HOST`_
-and `EMAIL_PORT`_ settings. The `EMAIL_HOST_USER`_ and `EMAIL_HOST_PASSWORD`_
-settings, if set, will be used to authenticate to the SMTP server and the
-`EMAIL_USE_TLS`_ settings will control whether a secure connection is used.
+Mail is sent using the SMTP host and port specified in the `EMAIL_HOST`_ and
+`EMAIL_PORT`_ settings. The `EMAIL_HOST_USER`_ and `EMAIL_HOST_PASSWORD`_
+settings, if set, are used to authenticate to the SMTP server, and the
+`EMAIL_USE_TLS`_ setting controls whether a secure connection is used.
.. note::
- The character set of email sent with ``django.core.mail`` will be set to
+ The character set of e-mail sent with ``django.core.mail`` will be set to
the value of your `DEFAULT_CHARSET setting`_.
.. _DEFAULT_CHARSET setting: ../settings/#default-charset
@@ -37,7 +37,6 @@ settings, if set, will be used to authenticate to the SMTP server and the
.. _EMAIL_HOST_PASSWORD: ../settings/#email-host-password
.. _EMAIL_USE_TLS: ../settings/#email-use-tls
-
send_mail()
===========
@@ -193,57 +192,64 @@ The EmailMessage and SMTPConnection classes
Django's ``send_mail()`` and ``send_mass_mail()`` functions are actually thin
wrappers that make use of the ``EmailMessage`` and ``SMTPConnection`` classes
-in ``django.mail``. If you ever need to customize the way Django sends email,
-you can subclass these two classes to suit your needs.
+in ``django.core.mail``. If you ever need to customize the way Django sends
+e-mail, you can subclass these two classes to suit your needs.
.. note::
Not all features of the ``EmailMessage`` class are available through the
``send_mail()`` and related wrapper functions. If you wish to use advanced
- features such as including BCC recipients or multi-part email, you will
- need to create ``EmailMessage`` instances directly.
+ features, such as BCC'ed recipients or multi-part e-mail, you'll need to
+ create ``EmailMessage`` instances directly.
-In general, ``EmailMessage`` is responsible for creating the email message
+In general, ``EmailMessage`` is responsible for creating the e-mail message
itself. ``SMTPConnection`` is responsible for the network connection side of
the operation. This means you can reuse the same connection (an
``SMTPConnection`` instance) for multiple messages.
-The ``EmailMessage`` class is initialised as follows::
+The ``EmailMessage`` class is initialized as follows::
email = EmailMessage(subject, body, from_email, to, bcc, connection)
All of these parameters are optional. If ``from_email`` is omitted, the value
from ``settings.DEFAULT_FROM_EMAIL`` is used. Both the ``to`` and ``bcc``
-parameters are lists of addresses.
-
-The class has the following methods that you can use:
-
- * ``send()`` sends the message, using either the connection that is specified
- in the ``connection`` attribute, or creating a new connection if none already
- exists.
- * ``message()`` constructs a ``django.core.mail.SafeMIMEText`` object (a
- sub-class of Python's ``email.MIMEText.MIMEText`` class) holding the
- message to be sent. If you ever need to extend the `EmailMessage` class,
- you will probably want to override this method to put the content you wish
- into the MIME object.
- * ``recipients()`` returns a lists of all the recipients of the message,
- whether they are recorded in the ``to`` or ``bcc`` attributes. This is
- another method you need to possibly override when sub-classing, since the
- SMTP server needs to be told the full list of recipients when the message
- is sent. If you add another way to specify recipients in your class, they
- need to be returned from this method as well.
+parameters are lists of addresses, as strings.
+
+For example::
+
+ email = EmailMessage('Hello', 'Body goes here', 'from@example.com',
+ ['to1@example.com', 'to2@example.com'],
+ ['bcc@example.com'])
+
+The class has the following methods:
+
+ * ``send()`` sends the message, using either the connection that is
+ specified in the ``connection`` attribute, or creating a new connection
+ if none already exists.
+
+ * ``message()`` constructs a ``django.core.mail.SafeMIMEText`` object (a
+ sub-class of Python's ``email.MIMEText.MIMEText`` class) holding the
+ message to be sent. If you ever need to extend the `EmailMessage` class,
+ you'll probably want to override this method to put the content you wish
+ into the MIME object.
+
+ * ``recipients()`` returns a list of all the recipients of the message,
+ whether they're recorded in the ``to`` or ``bcc`` attributes. This is
+ another method you might need to override when sub-classing, because the
+ SMTP server needs to be told the full list of recipients when the message
+ is sent. If you add another way to specify recipients in your class, they
+ need to be returned from this method as well.
The ``SMTPConnection`` class is initialized with the host, port, username and
password for the SMTP server. If you don't specify one or more of those
options, they are read from your settings file.
-If you are sending lots of messages at once, the ``send_messages()`` method of
-the ``SMTPConnection`` class will be useful. It takes a list of ``EmailMessage``
-instances (or sub-classes) and sends them over a single connection. For
-example, if you have a function called ``get_notification_email()`` that returns a
-list of ``EmailMessage`` objects representing some periodic email you wish to
+If you're sending lots of messages at once, the ``send_messages()`` method of
+the ``SMTPConnection`` class is useful. It takes a list of ``EmailMessage``
+instances (or subclasses) and sends them over a single connection. For example,
+if you have a function called ``get_notification_email()`` that returns a
+list of ``EmailMessage`` objects representing some periodic e-mail you wish to
send out, you could send this with::
connection = SMTPConnection() # Use default settings for connection
messages = get_notification_email()
connection.send_messages(messages)
-
diff --git a/docs/testing.txt b/docs/testing.txt
index a135734144..b3b33e9678 100644
--- a/docs/testing.txt
+++ b/docs/testing.txt
@@ -2,19 +2,29 @@
Testing Django applications
===========================
-Automated testing is an extremely useful weapon in the bug-killing arsenal
-of the modern developer. When initially writing code, a test suite can be
-used to validate that code behaves as expected. When refactoring or
-modifying code, tests serve as a guide to ensure that behavior hasn't
-changed unexpectedly as a result of the refactor.
-
-Testing a web application is a complex task, as there are many
-components of a web application that must be validated and tested. To
-help you test your application, Django provides a test execution
-framework, and range of utilities that can be used to simulate and
-inspect various facets of a web application.
-
- This testing framework is currently under development, and may change
+Automated testing is an extremely useful bug-killing tool for the modern
+Web developer. You can use a collection of tests -- a **test suite** -- to
+to solve, or avoid, a number of problems:
+
+ * When you're writing new code, you can use tests to validate your code
+ works as expected.
+
+ * When you're refactoring or modifying old code, you can use tests to
+ ensure your changes haven't affected your application's behavior
+ unexpectedly.
+
+Testing a Web application is a complex task, because a Web application is made
+of several layers of logic -- from HTTP-level request handling, to form
+validation and processing, to template rendering. With Django's test-execution
+framework and assorted utilities, you can simulate requests, insert test data,
+inspect your application's output and generally verify your code is doing what
+it should be doing.
+
+The best part is, it's really easy.
+
+.. admonition:: Note
+
+ This testing framework is currently under development. It may change
slightly before the next official Django release.
(That's *no* excuse not to write tests, though!)
@@ -166,7 +176,7 @@ To assist in testing various features of your application, Django provides
tools that can be used to establish tests and test conditions.
* `Test Client`_
-* Fixtures_
+* `TestCase`_
Test Client
-----------
@@ -216,21 +226,21 @@ can be invoked on the ``Client`` instance.
``post(path, data={}, content_type=MULTIPART_CONTENT)``
Make a POST request on the provided ``path``. If you provide a content type
- (e.g., ``text/xml`` for an XML payload), the contents of ``data`` will be
- sent as-is in the POST request, using the content type in the HTTP
+ (e.g., ``text/xml`` for an XML payload), the contents of ``data`` will be
+ sent as-is in the POST request, using the content type in the HTTP
``Content-Type`` header.
-
- If you do not provide a value for ``content_type``, the values in
+
+ If you do not provide a value for ``content_type``, the values in
``data`` will be transmitted with a content type of ``multipart/form-data``.
The key-value pairs in the data dictionary will be encoded as a multipart
message and used to create the POST data payload.
-
- To submit multiple values for a given key (for example, to specify
- the selections for a multiple selection list), provide the values as a
+
+ To submit multiple values for a given key (for example, to specify
+ the selections for a multiple selection list), provide the values as a
list or tuple for the required key. For example, a data dictionary of
``{'choices': ('a','b','d')}`` would submit three selected rows for the
field named ``choices``.
-
+
Submitting files is a special case. To POST a file, you need only
provide the file field name as a key, and a file handle to the file you wish to
upload as a value. The Test Client will populate the two POST fields (i.e.,
@@ -246,22 +256,35 @@ can be invoked on the ``Client`` instance.
file name), and `attachment_file` (containing the file data). Note that you
need to manually close the file after it has been provided to the POST.
-``login(path, username, password)``
- In a production site, it is likely that some views will be protected with
- the @login_required decorator provided by ``django.contrib.auth``. Interacting
- with a URL that has been login protected is a slightly complex operation,
- so the Test Client provides a simple method to automate the login process. A
- call to ``login()`` stimulates the series of GET and POST calls required
- to log a user into a @login_required protected view.
+``login(**credentials)``
+ ** New in Django development version **
+
+ On a production site, it is likely that some views will be protected from
+ anonymous access through the use of the @login_required decorator, or some
+ other login checking mechanism. The ``login()`` method can be used to
+ simulate the effect of a user logging into the site. As a result of calling
+ this method, the Client will have all the cookies and session data required
+ to pass any login-based tests that may form part of a view.
+
+ In most cases, the ``credentials`` required by this method are the username
+ and password of the user that wants to log in, provided as keyword
+ arguments::
+
+ c = Client()
+ c.login(username='fred', password='secret')
+ # Now you can access a login protected view
+
+ If you are using a different authentication backend, this method may
+ require different credentials.
- If login is possible, the final return value of ``login()`` is the response
- that is generated by issuing a GET request on the protected URL. If login
- is not possible, ``login()`` returns False.
+ ``login()`` returns ``True`` if it the credentials were accepted and login
+ was successful.
Note that since the test suite will be executed using the test database,
- which contains no users by default. As a result, logins for your production
- site will not work. You will need to create users as part of the test suite
- to be able to test logins to your application.
+ which contains no users by default. As a result, logins that are valid
+ on your production site will not work under test conditions. You will
+ need to create users as part of the test suite (either manually, or
+ using a test fixture).
Testing Responses
~~~~~~~~~~~~~~~~~
@@ -357,9 +380,31 @@ The following is a simple unit test using the Test Client::
# Check that the rendered context contains 5 customers
self.failUnlessEqual(len(response.context['customers']), 5)
-Fixtures
+TestCase
--------
+Normal python unit tests extend a base class of ``unittest.testCase``.
+Django provides an extension of this base class - ``django.test.TestCase``
+- that provides some additional capabilities that can be useful for
+testing web sites.
+
+Moving from a normal unittest TestCase to a Django TestCase is easy - just
+change the base class of your test from ``unittest.TestCase`` to
+``django.test.TestCase``. All of the standard Python unit test facilities
+will continue to be available, but they will be augmented with some useful
+extra facilities.
+
+Default Test Client
+~~~~~~~~~~~~~~~~~~~
+** New in Django development version **
+
+Every test case in a ``django.test.TestCase`` instance has access to an
+instance of a Django `Test Client`_. This Client can be accessed as
+``self.client``. This client is recreated for each test.
+
+Fixture loading
+~~~~~~~~~~~~~~~
+
A test case for a database-backed website isn't much use if there isn't any
data in the database. To make it easy to put test data into the database,
Django provides a fixtures framework.
@@ -370,22 +415,20 @@ comprise the fixture can be distributed over multiple directories, in
multiple applications.
.. note::
- If you have synchronized a Django project, you have already experienced
+ If you have synchronized a Django project, you have already experienced
the use of one fixture -- the ``initial_data`` fixture. Every time you
synchronize the database, Django installs the ``initial_data`` fixture.
This provides a mechanism to populate a new database with any initial
data (such as a default set of categories). Fixtures with other names
- can be installed manually using ``django-admin.py loaddata``.
-
+ can be installed manually using ``django-admin.py loaddata``.
-However, for the purposes of unit testing, each test must be able to
+However, for the purposes of unit testing, each test must be able to
guarantee the contents of the database at the start of each and every
-test. To do this, Django provides a TestCase baseclass that can integrate
-with fixtures.
+test.
-Moving from a normal unittest TestCase to a Django TestCase is easy - just
-change the base class of your test, and define a list of fixtures
-to be used. For example, the test case from `Writing unittests`_ would
+To define a fixture for a test, all you need to do is add a class
+attribute to your test describing the fixtures you want the test to use.
+For example, the test case from `Writing unittests`_ would
look like::
from django.test import TestCase
@@ -393,23 +436,62 @@ look like::
class AnimalTestCase(TestCase):
fixtures = ['mammals.json', 'birds']
-
+
def setUp(self):
# test definitions as before
At the start of each test case, before ``setUp()`` is run, Django will
-flush the database, returning the database the state it was in directly
-after ``syncdb`` was called. Then, all the named fixtures are installed.
+flush the database, returning the database the state it was in directly
+after ``syncdb`` was called. Then, all the named fixtures are installed.
In this example, any JSON fixture called ``mammals``, and any fixture
-named ``birds`` will be installed. See the documentation on
+named ``birds`` will be installed. See the documentation on
`loading fixtures`_ for more details on defining and installing fixtures.
.. _`loading fixtures`: ../django-admin/#loaddata-fixture-fixture
-This flush/load procedure is repeated for each test in the test case, so you
-can be certain that the outcome of a test will not be affected by
+This flush/load procedure is repeated for each test in the test case, so you
+can be certain that the outcome of a test will not be affected by
another test, or the order of test execution.
+Assertions
+~~~~~~~~~~
+** New in Django development version **
+
+Normal Python unit tests have a wide range of assertions, such as
+``assertTrue`` and ``assertEquals`` that can be used to validate behavior.
+``django.TestCase`` adds to these, providing some assertions
+that can be useful in testing the behavior of web sites.
+
+``assertContains(response, text, count=1)``
+ Assert that a response indicates that a page was retrieved successfully,
+ (i.e., the HTTP status code was 200), and that ``text`` occurs ``count``
+ times in the content of the response.
+
+``assertFormError(response, form, field, errors)``
+ Assert that a field on a form raised the provided list of errors when
+ rendered on the form.
+
+ ``form`` is the name the form object was given in the template context.
+
+ ``field`` is the name of the field on the form to check. If ``field``
+ has a value of ``None``, non-field errors will be checked.
+
+ ``errors`` is an error string, or a list of error strings, that are
+ expected as a result of form validation.
+
+``assertTemplateNotUsed(response, template_name)``
+ Assert that the template with the given name was *not* used in rendering
+ the response.
+
+``assertRedirects(response, expected_path)``
+ Assert that the response received redirects the browser to the provided
+ path, and that the expected_path can be retrieved.
+
+``assertTemplateUsed(response, template_name)``
+ Assert that the template with the given name was used in rendering the
+ response.
+
+
Running tests
=============
@@ -468,11 +550,11 @@ failed::
FAILED (failures=1)
-The return code for the script is the total number of failed and erroneous
+The return code for the script is the total number of failed and erroneous
tests. If all the tests pass, the return code is 0.
Regardless of whether the tests pass or fail, the test database is destroyed when
-all the tests have been executed.
+all the tests have been executed.
Using a different testing framework
===================================
@@ -483,7 +565,7 @@ it does provide a mechanism to allow you to invoke tests constructed for
an alternative framework as if they were normal Django tests.
When you run ``./manage.py test``, Django looks at the ``TEST_RUNNER``
-setting to determine what to do. By default, ``TEST_RUNNER`` points to
+setting to determine what to do. By default, ``TEST_RUNNER`` points to
``django.test.simple.run_tests``. This method defines the default Django
testing behavior. This behavior involves:
@@ -513,7 +595,7 @@ arguments:
Verbosity determines the amount of notification and debug information that
will be printed to the console; `0` is no output, `1` is normal output,
and `2` is verbose output.
-
+
This method should return the number of tests that failed.
Testing utilities
diff --git a/tests/modeltests/custom_columns/models.py b/tests/modeltests/custom_columns/models.py
index b2b7261c89..1283da07cf 100644
--- a/tests/modeltests/custom_columns/models.py
+++ b/tests/modeltests/custom_columns/models.py
@@ -71,7 +71,7 @@ __test__ = {'API_TESTS':"""
>>> Author.objects.filter(firstname__exact='John')
Traceback (most recent call last):
...
-TypeError: Cannot resolve keyword 'firstname' into field, choices are: article, id, first_name, last_name
+TypeError: Cannot resolve keyword 'firstname' into field. Choices are: article, id, first_name, last_name
>>> a = Author.objects.get(last_name__exact='Smith')
>>> a.first_name
diff --git a/tests/modeltests/lookup/models.py b/tests/modeltests/lookup/models.py
index c28f0e015f..c634aef8a1 100644
--- a/tests/modeltests/lookup/models.py
+++ b/tests/modeltests/lookup/models.py
@@ -223,11 +223,11 @@ DoesNotExist: Article matching query does not exist.
>>> Article.objects.filter(pub_date_year='2005').count()
Traceback (most recent call last):
...
-TypeError: Cannot resolve keyword 'pub_date_year' into field, choices are: id, headline, pub_date
+TypeError: Cannot resolve keyword 'pub_date_year' into field. Choices are: id, headline, pub_date
>>> Article.objects.filter(headline__starts='Article')
Traceback (most recent call last):
...
-TypeError: Cannot resolve keyword 'headline__starts' into field, choices are: id, headline, pub_date
+TypeError: Cannot resolve keyword 'headline__starts' into field. Choices are: id, headline, pub_date
"""}
diff --git a/tests/modeltests/many_to_one/models.py b/tests/modeltests/many_to_one/models.py
index 3ed449d598..02f7bf1066 100644
--- a/tests/modeltests/many_to_one/models.py
+++ b/tests/modeltests/many_to_one/models.py
@@ -174,13 +174,13 @@ False
>>> Article.objects.filter(reporter_id__exact=1)
Traceback (most recent call last):
...
-TypeError: Cannot resolve keyword 'reporter_id' into field, choices are: id, headline, pub_date, reporter
+TypeError: Cannot resolve keyword 'reporter_id' into field. Choices are: id, headline, pub_date, reporter
# You need to specify a comparison clause
>>> Article.objects.filter(reporter_id=1)
Traceback (most recent call last):
...
-TypeError: Cannot resolve keyword 'reporter_id' into field, choices are: id, headline, pub_date, reporter
+TypeError: Cannot resolve keyword 'reporter_id' into field. Choices are: id, headline, pub_date, reporter
# You can also instantiate an Article by passing
# the Reporter's ID instead of a Reporter object.
diff --git a/tests/modeltests/reverse_lookup/models.py b/tests/modeltests/reverse_lookup/models.py
index d30269c5c6..4d6591551a 100644
--- a/tests/modeltests/reverse_lookup/models.py
+++ b/tests/modeltests/reverse_lookup/models.py
@@ -55,5 +55,5 @@ __test__ = {'API_TESTS':"""
>>> Poll.objects.get(choice__name__exact="This is the answer")
Traceback (most recent call last):
...
-TypeError: Cannot resolve keyword 'choice' into field, choices are: poll_choice, related_choice, id, question, creator
+TypeError: Cannot resolve keyword 'choice' into field. Choices are: poll_choice, related_choice, id, question, creator
"""}
diff --git a/tests/modeltests/test_client/models.py b/tests/modeltests/test_client/models.py
index 44ddffb55f..cd8dbe37d2 100644
--- a/tests/modeltests/test_client/models.py
+++ b/tests/modeltests/test_client/models.py
@@ -24,20 +24,22 @@ from django.test import Client, TestCase
class ClientTest(TestCase):
fixtures = ['testdata.json']
- def setUp(self):
- "Set up test environment"
- self.client = Client()
-
def test_get_view(self):
"GET a view"
response = self.client.get('/test_client/get_view/')
# Check some response details
- self.assertEqual(response.status_code, 200)
+ self.assertContains(response, 'This is a test')
self.assertEqual(response.context['var'], 42)
self.assertEqual(response.template.name, 'GET Template')
- self.failUnless('This is a test.' in response.content)
+ def test_no_template_view(self):
+ "Check that template usage assersions work then templates aren't in use"
+ response = self.client.get('/test_client/no_template_view/')
+
+ # Check that the no template case doesn't mess with the template assertions
+ self.assertTemplateNotUsed(response, 'GET Template')
+
def test_get_post_view(self):
"GET a view that normally expects POSTs"
response = self.client.get('/test_client/post_view/', {})
@@ -45,6 +47,8 @@ class ClientTest(TestCase):
# Check some response details
self.assertEqual(response.status_code, 200)
self.assertEqual(response.template.name, 'Empty GET Template')
+ self.assertTemplateUsed(response, 'Empty GET Template')
+ self.assertTemplateNotUsed(response, 'Empty POST Template')
def test_empty_post(self):
"POST an empty dictionary to a view"
@@ -53,6 +57,8 @@ class ClientTest(TestCase):
# Check some response details
self.assertEqual(response.status_code, 200)
self.assertEqual(response.template.name, 'Empty POST Template')
+ self.assertTemplateNotUsed(response, 'Empty GET Template')
+ self.assertTemplateUsed(response, 'Empty POST Template')
def test_post(self):
"POST some data to a view"
@@ -80,7 +86,7 @@ class ClientTest(TestCase):
response = self.client.get('/test_client/redirect_view/')
# Check that the response was a 302 (redirect)
- self.assertEqual(response.status_code, 302)
+ self.assertRedirects(response, '/test_client/get_view/')
def test_valid_form(self):
"POST valid data to a form"
@@ -93,7 +99,7 @@ class ClientTest(TestCase):
}
response = self.client.post('/test_client/form_view/', post_data)
self.assertEqual(response.status_code, 200)
- self.assertEqual(response.template.name, "Valid POST Template")
+ self.assertTemplateUsed(response, "Valid POST Template")
def test_incomplete_data_form(self):
"POST incomplete data to a form"
@@ -102,8 +108,13 @@ class ClientTest(TestCase):
'value': 37
}
response = self.client.post('/test_client/form_view/', post_data)
+ self.assertContains(response, 'This field is required.', 3)
self.assertEqual(response.status_code, 200)
- self.assertEqual(response.template.name, "Invalid POST Template")
+ self.assertTemplateUsed(response, "Invalid POST Template")
+
+ self.assertFormError(response, 'form', 'email', 'This field is required.')
+ self.assertFormError(response, 'form', 'single', 'This field is required.')
+ self.assertFormError(response, 'form', 'multi', 'This field is required.')
def test_form_error(self):
"POST erroneous data to a form"
@@ -116,7 +127,57 @@ class ClientTest(TestCase):
}
response = self.client.post('/test_client/form_view/', post_data)
self.assertEqual(response.status_code, 200)
- self.assertEqual(response.template.name, "Invalid POST Template")
+ self.assertTemplateUsed(response, "Invalid POST Template")
+
+ self.assertFormError(response, 'form', 'email', 'Enter a valid e-mail address.')
+
+ def test_valid_form_with_template(self):
+ "POST valid data to a form using multiple templates"
+ post_data = {
+ 'text': 'Hello World',
+ 'email': 'foo@example.com',
+ 'value': 37,
+ 'single': 'b',
+ 'multi': ('b','c','e')
+ }
+ response = self.client.post('/test_client/form_view_with_template/', post_data)
+ self.assertContains(response, 'POST data OK')
+ self.assertTemplateUsed(response, "form_view.html")
+ self.assertTemplateUsed(response, 'base.html')
+ self.assertTemplateNotUsed(response, "Valid POST Template")
+
+ def test_incomplete_data_form_with_template(self):
+ "POST incomplete data to a form using multiple templates"
+ post_data = {
+ 'text': 'Hello World',
+ 'value': 37
+ }
+ response = self.client.post('/test_client/form_view_with_template/', post_data)
+ self.assertContains(response, 'POST data has errors')
+ self.assertTemplateUsed(response, 'form_view.html')
+ self.assertTemplateUsed(response, 'base.html')
+ self.assertTemplateNotUsed(response, "Invalid POST Template")
+
+ self.assertFormError(response, 'form', 'email', 'This field is required.')
+ self.assertFormError(response, 'form', 'single', 'This field is required.')
+ self.assertFormError(response, 'form', 'multi', 'This field is required.')
+
+ def test_form_error_with_template(self):
+ "POST erroneous data to a form using multiple templates"
+ post_data = {
+ 'text': 'Hello World',
+ 'email': 'not an email address',
+ 'value': 37,
+ 'single': 'b',
+ 'multi': ('b','c','e')
+ }
+ response = self.client.post('/test_client/form_view_with_template/', post_data)
+ self.assertContains(response, 'POST data has errors')
+ self.assertTemplateUsed(response, "form_view.html")
+ self.assertTemplateUsed(response, 'base.html')
+ self.assertTemplateNotUsed(response, "Invalid POST Template")
+
+ self.assertFormError(response, 'form', 'email', 'Enter a valid e-mail address.')
def test_unknown_page(self):
"GET an invalid URL"
@@ -130,20 +191,21 @@ class ClientTest(TestCase):
# Get the page without logging in. Should result in 302.
response = self.client.get('/test_client/login_protected_view/')
- self.assertEqual(response.status_code, 302)
+ self.assertRedirects(response, '/accounts/login/')
+ # Log in
+ self.client.login(username='testclient', password='password')
+
# Request a page that requires a login
- response = self.client.login('/test_client/login_protected_view/', 'testclient', 'password')
- self.failUnless(response)
+ response = self.client.get('/test_client/login_protected_view/')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.context['user'].username, 'testclient')
- self.assertEqual(response.template.name, 'Login Template')
def test_view_with_bad_login(self):
"Request a page that is protected with @login, but use bad credentials"
- response = self.client.login('/test_client/login_protected_view/', 'otheruser', 'nopassword')
- self.failIf(response)
+ login = self.client.login(username='otheruser', password='nopassword')
+ self.failIf(login)
def test_session_modifying_view(self):
"Request a page that modifies the session"
diff --git a/tests/modeltests/test_client/urls.py b/tests/modeltests/test_client/urls.py
index 707ecc186d..f63c486d01 100644
--- a/tests/modeltests/test_client/urls.py
+++ b/tests/modeltests/test_client/urls.py
@@ -2,11 +2,13 @@ from django.conf.urls.defaults import *
import views
urlpatterns = patterns('',
+ (r'^no_template_view/$', views.no_template_view),
(r'^get_view/$', views.get_view),
(r'^post_view/$', views.post_view),
(r'^raw_post_view/$', views.raw_post_view),
(r'^redirect_view/$', views.redirect_view),
(r'^form_view/$', views.form_view),
+ (r'^form_view_with_template/$', views.form_view_with_template),
(r'^login_protected_view/$', views.login_protected_view),
(r'^session_view/$', views.session_view),
(r'^broken_view/$', views.broken_view)
diff --git a/tests/modeltests/test_client/views.py b/tests/modeltests/test_client/views.py
index 36ec144cf6..3b7a57f4d0 100644
--- a/tests/modeltests/test_client/views.py
+++ b/tests/modeltests/test_client/views.py
@@ -4,6 +4,11 @@ from django.http import HttpResponse, HttpResponseRedirect
from django.contrib.auth.decorators import login_required
from django.newforms.forms import Form
from django.newforms import fields
+from django.shortcuts import render_to_response
+
+def no_template_view(request):
+ "A simple view that expects a GET request, and returns a rendered template"
+ return HttpResponse("No template used")
def get_view(request):
"A simple view that expects a GET request, and returns a rendered template"
@@ -79,6 +84,25 @@ def form_view(request):
c = Context({'form': form})
return HttpResponse(t.render(c))
+
+def form_view_with_template(request):
+ "A view that tests a simple form"
+ if request.method == 'POST':
+ form = TestForm(request.POST)
+ if form.is_valid():
+ message = 'POST data OK'
+ else:
+ message = 'POST data has errors'
+ else:
+ form = TestForm()
+ message = 'GET form page'
+ return render_to_response('form_view.html',
+ {
+ 'form': form,
+ 'message': message
+ }
+ )
+
def login_protected_view(request):
"A simple view that is login protected."
diff --git a/tests/regressiontests/null_queries/models.py b/tests/regressiontests/null_queries/models.py
index 4396ab4005..21944d9e7a 100644
--- a/tests/regressiontests/null_queries/models.py
+++ b/tests/regressiontests/null_queries/models.py
@@ -32,7 +32,7 @@ __test__ = {'API_TESTS':"""
>>> Choice.objects.filter(foo__exact=None)
Traceback (most recent call last):
...
-TypeError: Cannot resolve keyword 'foo' into field, choices are: id, poll, choice
+TypeError: Cannot resolve keyword 'foo' into field. Choices are: id, poll, choice
# Can't use None on anything other than __exact
>>> Choice.objects.filter(id__gt=None)
diff --git a/tests/templates/base.html b/tests/templates/base.html
new file mode 100644
index 0000000000..611bc094a9
--- /dev/null
+++ b/tests/templates/base.html
@@ -0,0 +1,8 @@
+<html>
+<head></head>
+<body>
+<h1>Django Internal Tests: {% block title %}{% endblock %}</h1>
+{% block content %}
+{% endblock %}
+</body>
+</html> \ No newline at end of file
diff --git a/tests/templates/form_view.html b/tests/templates/form_view.html
new file mode 100644
index 0000000000..1487217547
--- /dev/null
+++ b/tests/templates/form_view.html
@@ -0,0 +1,15 @@
+{% extends "base.html" %}
+{% block title %}Submit data{% endblock %}
+{% block content %}
+<h1>{{ message }}</h1>
+<form method='post' action='.'>
+{% if form.errors %}
+<p class='warning'>Please correct the errors below:</p>
+{% endif %}
+<ul class='form'>
+{{ form }}
+<li><input type='submit' value='Submit'></li>
+</ul>
+</form>
+
+{% endblock %} \ No newline at end of file
diff --git a/tests/templates/login.html b/tests/templates/login.html
index 8a0974c9a1..d55e9ddc75 100644
--- a/tests/templates/login.html
+++ b/tests/templates/login.html
@@ -1,7 +1,6 @@
-<html>
-<head></head>
-<body>
-<h1>Django Internal Tests: Login</h1>
+{% extends "base.html" %}
+{% block title %}Login{% endblock %}
+{% block content %}
{% if form.has_errors %}
<p>Your username and password didn't match. Please try again.</p>
{% endif %}
@@ -15,5 +14,4 @@
<input type="submit" value="login" />
<input type="hidden" name="next" value="{{ next }}" />
</form>
-</body>
-</html> \ No newline at end of file
+{% endblock %} \ No newline at end of file