mirror of
https://github.com/pacnpal/django-anymail.git
synced 2025-12-20 03:41:05 -05:00
Clean up and document Anymail's test EmailBackend
* Change Anymail's test EmailBackend to collect sent messages in django.core.mail.outbox, same as Django's own locmem EmailBackend. (So Django's test runner will automatically clear accumulated mail between test cases.) * Rename EmailMessage `test_response` attr to `anymail_test_response` to avoid conflicts, and record merged ESP send params in new `anymail_send_params` attr. * Add docs Closes #36.
This commit is contained in:
@@ -1,37 +1,42 @@
|
|||||||
from anymail.exceptions import AnymailAPIError
|
from django.core import mail
|
||||||
from anymail.message import AnymailRecipientStatus
|
|
||||||
|
|
||||||
from .base import AnymailBaseBackend, BasePayload
|
from .base import AnymailBaseBackend, BasePayload
|
||||||
|
from ..exceptions import AnymailAPIError
|
||||||
|
from ..message import AnymailRecipientStatus
|
||||||
from ..utils import get_anymail_setting
|
from ..utils import get_anymail_setting
|
||||||
|
|
||||||
|
|
||||||
class EmailBackend(AnymailBaseBackend):
|
class EmailBackend(AnymailBaseBackend):
|
||||||
"""
|
"""
|
||||||
Anymail backend that doesn't do anything.
|
Anymail backend that simulates sending messages, useful for testing.
|
||||||
|
|
||||||
Used for testing Anymail common backend functionality.
|
Sent messages are collected in django.core.mail.outbox (as with Django's locmem backend).
|
||||||
|
|
||||||
|
In addition:
|
||||||
|
* Anymail send params parsed from the message will be attached to the outbox message
|
||||||
|
as a dict in the attr `anymail_test_params`
|
||||||
|
* If the caller supplies an `anymail_test_response` attr on the message, that will be
|
||||||
|
used instead of the default "sent" response. It can be either an AnymailRecipientStatus
|
||||||
|
or an instance of AnymailAPIError (or a subclass) to raise an exception.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
esp_name = "Test"
|
esp_name = "Test"
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
# Init options from Django settings
|
|
||||||
esp_name = self.esp_name
|
|
||||||
self.sample_setting = get_anymail_setting('sample_setting', esp_name=esp_name,
|
|
||||||
kwargs=kwargs, allow_bare=True)
|
|
||||||
self.recorded_send_params = get_anymail_setting('recorded_send_params', default=[],
|
|
||||||
esp_name=esp_name, kwargs=kwargs)
|
|
||||||
super(EmailBackend, self).__init__(*args, **kwargs)
|
super(EmailBackend, self).__init__(*args, **kwargs)
|
||||||
|
if not hasattr(mail, 'outbox'):
|
||||||
|
mail.outbox = [] # see django.core.mail.backends.locmem
|
||||||
|
|
||||||
def build_message_payload(self, message, defaults):
|
def build_message_payload(self, message, defaults):
|
||||||
return TestPayload(backend=self, message=message, defaults=defaults)
|
return TestPayload(backend=self, message=message, defaults=defaults)
|
||||||
|
|
||||||
def post_to_esp(self, payload, message):
|
def post_to_esp(self, payload, message):
|
||||||
# Keep track of the send params (for test-case access)
|
# Keep track of the sent messages and params (for test cases)
|
||||||
self.recorded_send_params.append(payload.params)
|
message.anymail_test_params = payload.params
|
||||||
|
mail.outbox.append(message)
|
||||||
try:
|
try:
|
||||||
# Tests can supply their own message.test_response:
|
# Tests can supply their own message.test_response:
|
||||||
response = message.test_response
|
response = message.anymail_test_response
|
||||||
if isinstance(response, AnymailAPIError):
|
if isinstance(response, AnymailAPIError):
|
||||||
raise response
|
raise response
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
@@ -49,14 +54,6 @@ class EmailBackend(AnymailBaseBackend):
|
|||||||
raise AnymailAPIError('Unparsable test response')
|
raise AnymailAPIError('Unparsable test response')
|
||||||
|
|
||||||
|
|
||||||
# Pre-v0.8 naming (immediately deprecated for this undocumented test feature)
|
|
||||||
class TestBackend(object):
|
|
||||||
def __init__(self, **kwargs):
|
|
||||||
raise NotImplementedError(
|
|
||||||
"Anymail's (undocumented) TestBackend has been renamed to "
|
|
||||||
"'anymail.backends.test.EmailBackend'")
|
|
||||||
|
|
||||||
|
|
||||||
class TestPayload(BasePayload):
|
class TestPayload(BasePayload):
|
||||||
# For test purposes, just keep a dict of the params we've received.
|
# For test purposes, just keep a dict of the params we've received.
|
||||||
# (This approach is also useful for native API backends -- think of
|
# (This approach is also useful for native API backends -- think of
|
||||||
@@ -129,3 +126,16 @@ class TestPayload(BasePayload):
|
|||||||
def set_esp_extra(self, extra):
|
def set_esp_extra(self, extra):
|
||||||
# Merge extra into params
|
# Merge extra into params
|
||||||
self.params.update(extra)
|
self.params.update(extra)
|
||||||
|
|
||||||
|
|
||||||
|
class _EmailBackendWithRequiredSetting(EmailBackend):
|
||||||
|
"""Test backend with a required setting `sample_setting`.
|
||||||
|
|
||||||
|
Intended only for internal use by Anymail settings tests.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
esp_name = self.esp_name
|
||||||
|
self.sample_setting = get_anymail_setting('sample_setting', esp_name=esp_name,
|
||||||
|
kwargs=kwargs, allow_bare=True)
|
||||||
|
super(_EmailBackendWithRequiredSetting, self).__init__(*args, **kwargs)
|
||||||
|
|||||||
@@ -218,11 +218,13 @@ ESP send status
|
|||||||
if you try to access it.
|
if you try to access it.
|
||||||
|
|
||||||
This might cause problems in your test cases, because Django
|
This might cause problems in your test cases, because Django
|
||||||
`substitutes its own locmem email backend`_ during testing (so anymail_status
|
:ref:`substitutes its own locmem EmailBackend <django:topics-testing-email>`
|
||||||
won't be set even after sending). Your code should either guard against
|
during testing (so anymail_status never gets attached to the EmailMessage).
|
||||||
a missing anymail_status attribute, or use :class:`AnymailMessage`
|
If you run into this, you can: change your code to guard against
|
||||||
(or the :class:`AnymailMessageMixin`) which initializes its anymail_status
|
a missing anymail_status attribute; switch from using EmailMessage to
|
||||||
attribute to a default AnymailStatus object.
|
:class:`AnymailMessage` (or the :class:`AnymailMessageMixin`) to ensure the
|
||||||
|
anymail_status attribute is always there; or substitute
|
||||||
|
:ref:`Anymail's test backend <test-backend>` in any affected test cases.
|
||||||
|
|
||||||
After sending through an Anymail backend,
|
After sending through an Anymail backend,
|
||||||
:attr:`~AnymailMessage.anymail_status` will be an object with these attributes:
|
:attr:`~AnymailMessage.anymail_status` will be an object with these attributes:
|
||||||
@@ -321,10 +323,6 @@ ESP send status
|
|||||||
message.anymail_status.esp_response.json()
|
message.anymail_status.esp_response.json()
|
||||||
|
|
||||||
|
|
||||||
.. _substitutes its own locmem email backend:
|
|
||||||
https://docs.djangoproject.com/en/stable/topics/testing/tools/#email-services
|
|
||||||
|
|
||||||
|
|
||||||
.. _inline-images:
|
.. _inline-images:
|
||||||
|
|
||||||
Inline images
|
Inline images
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ done with Anymail:
|
|||||||
multiple_backends
|
multiple_backends
|
||||||
django_templates
|
django_templates
|
||||||
securing_webhooks
|
securing_webhooks
|
||||||
|
test_backend
|
||||||
|
|
||||||
.. TODO:
|
.. TODO:
|
||||||
.. Working with django-mailer(2)
|
.. Working with django-mailer(2)
|
||||||
|
|||||||
52
docs/tips/test_backend.rst
Normal file
52
docs/tips/test_backend.rst
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
.. _test-backend:
|
||||||
|
|
||||||
|
Testing your app
|
||||||
|
================
|
||||||
|
|
||||||
|
Django's own test runner makes sure your
|
||||||
|
:ref:`test cases don't send email <django:topics-testing-email>`,
|
||||||
|
by loading a dummy EmailBackend that accumulates messages
|
||||||
|
in memory rather than sending them. That works just fine with Anymail.
|
||||||
|
|
||||||
|
Anymail also includes its own "test" EmailBackend. This is intended primarily for
|
||||||
|
Anymail's own internal tests, but you may find it useful for some of your test cases, too:
|
||||||
|
|
||||||
|
* Like Django's locmem EmailBackend, Anymail's test EmailBackend collects sent messages
|
||||||
|
in :data:`django.core.mail.outbox`.
|
||||||
|
Django clears the outbox automatically between test cases.
|
||||||
|
See :ref:`email testing tools <django:topics-testing-email>` in the Django docs for more information.
|
||||||
|
|
||||||
|
* Unlike the locmem backend, Anymail's test backend processes the messages as though they
|
||||||
|
would be sent by a generic ESP. This means every sent EmailMessage will end up with an
|
||||||
|
:attr:`~anymail.message.AnymailMessage.anymail_status` attribute after sending,
|
||||||
|
and some common problems like malformed addresses may be detected.
|
||||||
|
(But no ESP-specific checks are run.)
|
||||||
|
|
||||||
|
* Anymail's test backend also adds an :attr:`anymail_send_params` attribute to each EmailMessage
|
||||||
|
as it sends it. This is a dict of the actual params that would be used to send the message,
|
||||||
|
including both Anymail-specific attributes from the EmailMessage and options that would
|
||||||
|
come from Anymail settings defaults.
|
||||||
|
|
||||||
|
Here's an example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from django.core import mail
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.test.utils import override_settings
|
||||||
|
|
||||||
|
@override_settings(EMAIL_BACKEND='anymail.backends.test.EmailBackend')
|
||||||
|
class SignupTestCase(TestCase):
|
||||||
|
# Assume our app has a signup view that accepts an email address...
|
||||||
|
def test_sends_confirmation_email(self):
|
||||||
|
self.client.post("/account/signup/", {"email": "user@example.com"})
|
||||||
|
|
||||||
|
# Test that one message was sent:
|
||||||
|
self.assertEqual(len(mail.outbox), 1)
|
||||||
|
|
||||||
|
# Verify attributes of the EmailMessage that was sent:
|
||||||
|
self.assertEqual(mail.outbox[0].to, ["user@example.com"])
|
||||||
|
self.assertEqual(mail.outbox[0].tags, ["confirmation"]) # an Anymail custom attr
|
||||||
|
|
||||||
|
# Or verify the Anymail params, including any merged settings defaults:
|
||||||
|
self.assertTrue(mail.outbox[0].anymail_send_params["track_clicks"])
|
||||||
@@ -2,6 +2,7 @@ from datetime import datetime
|
|||||||
from email.mime.text import MIMEText
|
from email.mime.text import MIMEText
|
||||||
|
|
||||||
import six
|
import six
|
||||||
|
from django.core import mail
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.core.mail import get_connection, send_mail
|
from django.core.mail import get_connection, send_mail
|
||||||
from django.test import SimpleTestCase
|
from django.test import SimpleTestCase
|
||||||
@@ -16,34 +17,36 @@ from anymail.message import AnymailMessage
|
|||||||
from .utils import AnymailTestMixin
|
from .utils import AnymailTestMixin
|
||||||
|
|
||||||
|
|
||||||
recorded_send_params = []
|
@override_settings(EMAIL_BACKEND='anymail.backends.test.EmailBackend')
|
||||||
|
|
||||||
|
|
||||||
@override_settings(EMAIL_BACKEND='anymail.backends.test.EmailBackend',
|
|
||||||
ANYMAIL_TEST_SAMPLE_SETTING='sample', # required test EmailBackend setting
|
|
||||||
ANYMAIL_TEST_RECORDED_SEND_PARAMS=recorded_send_params)
|
|
||||||
class TestBackendTestCase(SimpleTestCase, AnymailTestMixin):
|
class TestBackendTestCase(SimpleTestCase, AnymailTestMixin):
|
||||||
"""Base TestCase using Anymail's Test EmailBackend"""
|
"""Base TestCase using Anymail's Test EmailBackend"""
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(TestBackendTestCase, self).setUp()
|
super(TestBackendTestCase, self).setUp()
|
||||||
del recorded_send_params[:] # empty the list from previous tests
|
|
||||||
# Simple message useful for many tests
|
# Simple message useful for many tests
|
||||||
self.message = AnymailMessage('Subject', 'Text Body', 'from@example.com', ['to@example.com'])
|
self.message = AnymailMessage('Subject', 'Text Body', 'from@example.com', ['to@example.com'])
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_send_count():
|
def get_send_count():
|
||||||
"""Returns number of times "send api" has been called this test"""
|
"""Returns number of times "send api" has been called this test"""
|
||||||
return len(recorded_send_params)
|
try:
|
||||||
|
return len(mail.outbox)
|
||||||
|
except AttributeError:
|
||||||
|
return 0 # mail.outbox not initialized by either Anymail test or Django locmem backend
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_send_params():
|
def get_send_params():
|
||||||
"""Returns the params for the most recent "send api" call"""
|
"""Returns the params for the most recent "send api" call"""
|
||||||
return recorded_send_params[-1]
|
try:
|
||||||
|
return mail.outbox[-1].anymail_test_params
|
||||||
|
except IndexError:
|
||||||
|
raise IndexError("No messages have been sent through the Anymail test backend")
|
||||||
|
except AttributeError:
|
||||||
|
raise AttributeError("The last message sent was not processed through the Anymail test backend")
|
||||||
|
|
||||||
|
|
||||||
@override_settings(EMAIL_BACKEND='anymail.backends.test.EmailBackend') # but no ANYMAIL settings overrides
|
@override_settings(EMAIL_BACKEND='anymail.backends.test._EmailBackendWithRequiredSetting')
|
||||||
class BackendSettingsTests(SimpleTestCase, AnymailTestMixin): # (so not TestBackendTestCase)
|
class BackendSettingsTests(TestBackendTestCase):
|
||||||
"""Test settings initializations for Anymail EmailBackends"""
|
"""Test settings initializations for Anymail EmailBackends"""
|
||||||
|
|
||||||
@override_settings(ANYMAIL={'TEST_SAMPLE_SETTING': 'setting_from_anymail_settings'})
|
@override_settings(ANYMAIL={'TEST_SAMPLE_SETTING': 'setting_from_anymail_settings'})
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ class TestPostSendSignal(TestBackendTestCase):
|
|||||||
self.receiver_called = True
|
self.receiver_called = True
|
||||||
self.addCleanup(post_send.disconnect, receiver=handle_post_send)
|
self.addCleanup(post_send.disconnect, receiver=handle_post_send)
|
||||||
|
|
||||||
self.message.test_response = {
|
self.message.anymail_test_response = {
|
||||||
'recipient_status': {
|
'recipient_status': {
|
||||||
'to@example.com': AnymailRecipientStatus(message_id=None, status='rejected')
|
'to@example.com': AnymailRecipientStatus(message_id=None, status='rejected')
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user