Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Added redirection for email services during test conditions.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@5173 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit 469314e7bcad7390362804b73d74b1ff1ba6396b 1 parent 1c53661
@freakboy3742 freakboy3742 authored
View
22 django/test/testcases.py
@@ -1,7 +1,7 @@
import re, doctest, unittest
from urlparse import urlparse
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.test.client import Client
@@ -33,23 +33,27 @@ def report_unexpected_exception(self, out, test, example, exc_info):
transaction.rollback_unless_managed()
class TestCase(unittest.TestCase):
- def install_fixtures(self):
- """If the Test Case class has a 'fixtures' member, clear the database and
- install the named fixtures at the start of each test.
+ 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 run(self, result=None):
- """Wrapper around default run method so that user-defined Test Cases
- automatically call install_fixtures without having to include a call to
- super().
+ """Wrapper around default run 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.install_fixtures()
+ self._pre_setup()
super(TestCase, self).run(result)
def assertRedirects(self, response, expected_path):
View
32 django/test/utils.py
@@ -1,7 +1,7 @@
import sys, time
from django.conf import settings
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.test import signals
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)
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():
"""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.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
+ mail.SMTPConnection = mail.original_SMTPConnection
+ del mail.original_SMTPConnection
+
+ del mail.outbox
+
def _set_autocommit(connection):
"Make sure a connection is in autocommit mode."
if hasattr(connection.connection, "autocommit"):
View
70 docs/testing.txt
@@ -177,6 +177,7 @@ tools that can be used to establish tests and test conditions.
* `Test Client`_
* `TestCase`_
+* `Email services`_
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.
``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
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
~~~~~~~~~~~~~~~~~
-The ``get()``, ``post()`` and ``login()`` methods all return a Response
-object. This Response object has the following properties that can be used
-for testing purposes:
+The ``get()`` and ``post()`` methods both return a Response object. This
+Response object has the following properties that can be used for testing
+purposes:
=============== ==========================================================
Property Description
@@ -396,7 +397,7 @@ extra facilities.
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
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
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
~~~~~~~~~~
-** New in Django development version **
+**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.
@@ -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
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
=============
@@ -610,11 +663,12 @@ a number of utility methods in the ``django.test.utils`` module.
``setup_test_environment()``
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()``
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)``
Creates a new test database, and run ``syncdb`` against it.
View
34 tests/modeltests/test_client/models.py
@@ -20,6 +20,7 @@
"""
from django.test import Client, TestCase
+from django.core import mail
class ClientTest(TestCase):
fixtures = ['testdata.json']
@@ -232,3 +233,36 @@ def test_view_with_exception(self):
self.fail('Should raise an error')
except KeyError:
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
4 tests/modeltests/test_client/urls.py
@@ -11,5 +11,7 @@
(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)
+ (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
26 tests/modeltests/test_client/views.py
@@ -1,4 +1,5 @@
from xml.dom.minidom import parseString
+from django.core.mail import EmailMessage, SMTPConnection
from django.template import Context, Template
from django.http import HttpResponse, HttpResponseRedirect
from django.contrib.auth.decorators import login_required
@@ -124,3 +125,28 @@ def session_view(request):
def broken_view(request):
"""A view which just raises an exception, simulating a broken view."""
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")
Please sign in to comment.
Something went wrong with that request. Please try again.