Added redirection for email services during test conditions.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@5173 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Russell Keith-Magee 2007-05-08 11:19:34 +00:00
parent 1c53661bd1
commit 469314e7bc
6 changed files with 169 additions and 19 deletions

View File

@ -1,7 +1,7 @@
import re, doctest, unittest import re, doctest, unittest
from urlparse import urlparse from urlparse import urlparse
from django.db import transaction from django.db import transaction
from django.core import management from django.core import management, mail
from django.db.models import get_apps from django.db.models import get_apps
from django.test.client import Client from django.test.client import Client
@ -33,23 +33,27 @@ class DocTestRunner(doctest.DocTestRunner):
transaction.rollback_unless_managed() transaction.rollback_unless_managed()
class TestCase(unittest.TestCase): class TestCase(unittest.TestCase):
def install_fixtures(self): def _pre_setup(self):
"""If the Test Case class has a 'fixtures' member, clear the database and """Perform any pre-test setup. This includes:
install the named fixtures at the start of each test.
* 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) management.flush(verbosity=0, interactive=False)
if hasattr(self, 'fixtures'): if hasattr(self, 'fixtures'):
management.load_data(self.fixtures, verbosity=0) management.load_data(self.fixtures, verbosity=0)
mail.outbox = []
def run(self, result=None): def run(self, result=None):
"""Wrapper around default run method so that user-defined Test Cases """Wrapper around default run method to perform common Django test set up.
automatically call install_fixtures without having to include a call to This means that user-defined Test Cases aren't required to include a call
super(). to super().setUp().
""" """
self.client = Client() self.client = Client()
self.install_fixtures() self._pre_setup()
super(TestCase, self).run(result) super(TestCase, self).run(result)
def assertRedirects(self, response, expected_path): def assertRedirects(self, response, expected_path):

View File

@ -1,7 +1,7 @@
import sys, time import sys, time
from django.conf import settings from django.conf import settings
from django.db import connection, transaction, backend from django.db import connection, transaction, backend
from django.core import management from django.core import management, mail
from django.dispatch import dispatcher from django.dispatch import dispatcher
from django.test import signals from django.test import signals
from django.template import Template from django.template import Template
@ -18,24 +18,54 @@ def instrumented_test_render(self, context):
dispatcher.send(signal=signals.template_rendered, sender=self, template=self, context=context) dispatcher.send(signal=signals.template_rendered, sender=self, template=self, context=context)
return self.nodelist.render(context) return self.nodelist.render(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(): def setup_test_environment():
"""Perform any global pre-test setup. This involves: """Perform any global pre-test setup. This involves:
- Installing the instrumented test renderer - Installing the instrumented test renderer
- Diverting the email sending functions to a test buffer
""" """
Template.original_render = Template.render Template.original_render = Template.render
Template.render = instrumented_test_render Template.render = instrumented_test_render
mail.original_SMTPConnection = mail.SMTPConnection
mail.SMTPConnection = TestSMTPConnection
mail.outbox = []
def teardown_test_environment(): def teardown_test_environment():
"""Perform any global post-test teardown. This involves: """Perform any global post-test teardown. This involves:
- Restoring the original test renderer - Restoring the original test renderer
- Restoring the email sending functions
""" """
Template.render = Template.original_render Template.render = Template.original_render
del Template.original_render del Template.original_render
mail.SMTPConnection = mail.original_SMTPConnection
del mail.original_SMTPConnection
del mail.outbox
def _set_autocommit(connection): def _set_autocommit(connection):
"Make sure a connection is in autocommit mode." "Make sure a connection is in autocommit mode."
if hasattr(connection.connection, "autocommit"): if hasattr(connection.connection, "autocommit"):

View File

@ -177,6 +177,7 @@ tools that can be used to establish tests and test conditions.
* `Test Client`_ * `Test Client`_
* `TestCase`_ * `TestCase`_
* `Email services`_
Test Client Test Client
----------- -----------
@ -257,7 +258,7 @@ can be invoked on the ``Client`` instance.
need to manually close the file after it has been provided to the POST. need to manually close the file after it has been provided to the POST.
``login(**credentials)`` ``login(**credentials)``
** New in Django development version ** **New in Django development version**
On a production site, it is likely that some views will be protected from 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 anonymous access through the use of the @login_required decorator, or some
@ -289,9 +290,9 @@ can be invoked on the ``Client`` instance.
Testing Responses Testing Responses
~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~
The ``get()``, ``post()`` and ``login()`` methods all return a Response The ``get()`` and ``post()`` methods both return a Response object. This
object. This Response object has the following properties that can be used Response object has the following properties that can be used for testing
for testing purposes: purposes:
=============== ========================================================== =============== ==========================================================
Property Description Property Description
@ -396,7 +397,7 @@ extra facilities.
Default Test Client Default Test Client
~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~
** New in Django development version ** **New in Django development version**
Every test case in a ``django.test.TestCase`` instance has access to an 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 instance of a Django `Test Client`_. This Client can be accessed as
@ -453,9 +454,18 @@ 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 can be certain that the outcome of a test will not be affected by
another test, or the order of test execution. another test, or the order of test execution.
Emptying the test outbox
~~~~~~~~~~~~~~~~~~~~~~~~
**New in Django development version**
At the start of each test case, in addition to installing fixtures,
Django clears the contents of the test email outbox.
For more detail on email services during tests, see `Email services`_.
Assertions Assertions
~~~~~~~~~~ ~~~~~~~~~~
** New in Django development version ** **New in Django development version**
Normal Python unit tests have a wide range of assertions, such as Normal Python unit tests have a wide range of assertions, such as
``assertTrue`` and ``assertEquals`` that can be used to validate behavior. ``assertTrue`` and ``assertEquals`` that can be used to validate behavior.
@ -491,6 +501,49 @@ that can be useful in testing the behavior of web sites.
Assert that the template with the given name was used in rendering the Assert that the template with the given name was used in rendering the
response. response.
Email services
--------------
**New in Django development version**
If your view makes use of the `Django email services`_, you don't really
want email to be sent every time you run a test using that view.
When the Django test framework is initialized, it transparently replaces the
normal `SMTPConnection`_ class with a dummy implementation that redirects all
email to a dummy outbox. This outbox, stored as ``django.core.mail.outbox``,
is a simple list of all `EmailMessage`_ instances that have been sent.
For example, during test conditions, it would be possible to run the following
code::
from django.core import mail
# Send message
mail.send_mail('Subject here', 'Here is the message.', 'from@example.com',
['to@example.com'], fail_silently=False)
# One message has been sent
self.assertEqual(len(mail.outbox), 1)
# Subject of first message is correct
self.assertEqual(mail.outbox[0].subject, 'Subject here')
The ``mail.outbox`` object does not exist under normal execution conditions.
The outbox is created during test setup, along with the dummy `SMTPConnection`_.
When the test framework is torn down, the standard `SMTPConnection`_ class
is restored, and the test outbox is destroyed.
As noted `previously`_, the test outbox is emptied at the start of every
test in a Django TestCase. To empty the outbox manually, assign the empty list
to mail.outbox::
from django.core import mail
# Empty the test outbox
mail.outbox = []
.. _`Django email services`: ../email/
.. _`SMTPConnection`: ../email/#the-emailmessage-and-smtpconnection-classes
.. _`EmailMessage`: ../email/#the-emailmessage-and-smtpconnection-classes
.. _`previously`: #emptying-the-test-outbox
Running tests Running tests
============= =============
@ -610,11 +663,12 @@ a number of utility methods in the ``django.test.utils`` module.
``setup_test_environment()`` ``setup_test_environment()``
Performs any global pre-test setup, such as the installing the Performs any global pre-test setup, such as the installing the
instrumentation of the template rendering system. instrumentation of the template rendering system and setting up
the dummy SMTPConnection.
``teardown_test_environment()`` ``teardown_test_environment()``
Performs any global post-test teardown, such as removing the instrumentation Performs any global post-test teardown, such as removing the instrumentation
of the template rendering system. of the template rendering system and restoring normal email services.
``create_test_db(verbosity=1, autoclobber=False)`` ``create_test_db(verbosity=1, autoclobber=False)``
Creates a new test database, and run ``syncdb`` against it. Creates a new test database, and run ``syncdb`` against it.

View File

@ -20,6 +20,7 @@ rather than the HTML rendered to the end-user.
""" """
from django.test import Client, TestCase from django.test import Client, TestCase
from django.core import mail
class ClientTest(TestCase): class ClientTest(TestCase):
fixtures = ['testdata.json'] fixtures = ['testdata.json']
@ -232,3 +233,36 @@ class ClientTest(TestCase):
self.fail('Should raise an error') self.fail('Should raise an error')
except KeyError: except KeyError:
pass pass
def test_mail_sending(self):
"Test that mail is redirected to a dummy outbox during test setup"
response = self.client.get('/test_client/mail_sending_view/')
self.assertEqual(response.status_code, 200)
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].subject, 'Test message')
self.assertEqual(mail.outbox[0].body, 'This is a test email')
self.assertEqual(mail.outbox[0].from_email, 'from@example.com')
self.assertEqual(mail.outbox[0].to[0], 'first@example.com')
self.assertEqual(mail.outbox[0].to[1], 'second@example.com')
def test_mass_mail_sending(self):
"Test that mass mail is redirected to a dummy outbox during test setup"
response = self.client.get('/test_client/mass_mail_sending_view/')
self.assertEqual(response.status_code, 200)
self.assertEqual(len(mail.outbox), 2)
self.assertEqual(mail.outbox[0].subject, 'First Test message')
self.assertEqual(mail.outbox[0].body, 'This is the first test email')
self.assertEqual(mail.outbox[0].from_email, 'from@example.com')
self.assertEqual(mail.outbox[0].to[0], 'first@example.com')
self.assertEqual(mail.outbox[0].to[1], 'second@example.com')
self.assertEqual(mail.outbox[1].subject, 'Second Test message')
self.assertEqual(mail.outbox[1].body, 'This is the second test email')
self.assertEqual(mail.outbox[1].from_email, 'from@example.com')
self.assertEqual(mail.outbox[1].to[0], 'second@example.com')
self.assertEqual(mail.outbox[1].to[1], 'third@example.com')

View File

@ -11,5 +11,7 @@ urlpatterns = patterns('',
(r'^form_view_with_template/$', views.form_view_with_template), (r'^form_view_with_template/$', views.form_view_with_template),
(r'^login_protected_view/$', views.login_protected_view), (r'^login_protected_view/$', views.login_protected_view),
(r'^session_view/$', views.session_view), (r'^session_view/$', views.session_view),
(r'^broken_view/$', views.broken_view) (r'^broken_view/$', views.broken_view),
(r'^mail_sending_view/$', views.mail_sending_view),
(r'^mass_mail_sending_view/$', views.mass_mail_sending_view)
) )

View File

@ -1,4 +1,5 @@
from xml.dom.minidom import parseString from xml.dom.minidom import parseString
from django.core.mail import EmailMessage, SMTPConnection
from django.template import Context, Template from django.template import Context, Template
from django.http import HttpResponse, HttpResponseRedirect from django.http import HttpResponse, HttpResponseRedirect
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
@ -124,3 +125,28 @@ def session_view(request):
def broken_view(request): def broken_view(request):
"""A view which just raises an exception, simulating a broken view.""" """A view which just raises an exception, simulating a broken view."""
raise KeyError("Oops! Looks like you wrote some bad code.") raise KeyError("Oops! Looks like you wrote some bad code.")
def mail_sending_view(request):
EmailMessage(
"Test message",
"This is a test email",
"from@example.com",
['first@example.com', 'second@example.com']).send()
return HttpResponse("Mail sent")
def mass_mail_sending_view(request):
m1 = EmailMessage(
'First Test message',
'This is the first test email',
'from@example.com',
['first@example.com', 'second@example.com'])
m2 = EmailMessage(
'Second Test message',
'This is the second test email',
'from@example.com',
['second@example.com', 'third@example.com'])
c = SMTPConnection()
c.send_messages([m1,m2])
return HttpResponse("Mail sent")