mirror of https://github.com/django/django.git
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:
parent
1c53661bd1
commit
469314e7bc
|
@ -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):
|
||||||
|
|
|
@ -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"):
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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)
|
||||||
)
|
)
|
||||||
|
|
|
@ -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")
|
||||||
|
|
Loading…
Reference in New Issue