Skip to content

Commit d3730f0

Browse files
committed
Docs: cover testing tracking and inbound webhooks
1 parent a868bf3 commit d3730f0

File tree

4 files changed

+175
-62
lines changed

4 files changed

+175
-62
lines changed

CHANGELOG.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ Other
5353
server-level setting. (See
5454
`docs <https://anymail.readthedocs.io/en/latest/esps/postmark/#limitations-and-quirks>`__.)
5555

56+
* Expand `testing documentation <https://anymail.readthedocs.io/en/latest/tips/testing/>`__
57+
to cover tracking events and inbound handling, and to clarify test EmailBackend behavior.
58+
5659
* In Anymail's test EmailBackend, add `is_batch_send` boolean to `anymail_test_params`
5760
to help tests check whether a sent message would fall under Anymail's batch-send logic.
5861

docs/tips/index.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,5 @@ done with Anymail:
1111
multiple_backends
1212
django_templates
1313
securing_webhooks
14-
test_backend
14+
testing
1515
performance

docs/tips/test_backend.rst

Lines changed: 0 additions & 61 deletions
This file was deleted.

docs/tips/testing.rst

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
.. _testing:
2+
3+
Testing your app
4+
================
5+
6+
.. _test-backend:
7+
.. _testing-sending:
8+
9+
Testing sending mail
10+
--------------------
11+
12+
Django's documentation covers the basics of
13+
:ref:`testing email sending in Django <django:topics-testing-email>`.
14+
Everything in their examples will work with projects using Anymail.
15+
16+
Django's test runner makes sure your test cases don't actually send email,
17+
by loading a dummy "locmem" EmailBackend that accumulates messages
18+
in memory rather than sending them. You may not need anything more
19+
complicated for verifying your app.
20+
21+
Anymail also includes its own "test" EmailBackend. This is intended primarily for
22+
Anymail's internal testing, but you may find it useful for some of your test cases, too:
23+
24+
* Like Django's locmem EmailBackend, Anymail's test EmailBackend collects sent messages
25+
in :data:`django.core.mail.outbox`. Django clears the outbox automatically between test cases.
26+
27+
* Unlike the locmem backend, Anymail's test backend processes the messages as though they
28+
would be sent by a generic ESP. This means every sent EmailMessage will end up with an
29+
:attr:`~anymail.message.AnymailMessage.anymail_status` attribute after sending,
30+
and some common problems like malformed addresses may be detected.
31+
(But no ESP-specific checks are run.)
32+
33+
* Anymail's test backend also adds an :attr:`anymail_test_params` attribute to each EmailMessage
34+
as it sends it. This is a dict of the actual params that would be used to send the message,
35+
including both Anymail-specific attributes from the EmailMessage and options that would
36+
come from Anymail settings defaults.
37+
38+
Here's an example:
39+
40+
.. code-block:: python
41+
42+
from django.core import mail
43+
from django.test import TestCase
44+
from django.test.utils import override_settings
45+
46+
@override_settings(EMAIL_BACKEND='anymail.backends.test.EmailBackend')
47+
class SignupTestCase(TestCase):
48+
# Assume our app has a signup view that accepts an email address...
49+
def test_sends_confirmation_email(self):
50+
self.client.post("/account/signup/", {"email": "[email protected]"})
51+
52+
# Test that one message was sent:
53+
self.assertEqual(len(mail.outbox), 1)
54+
55+
# Verify attributes of the EmailMessage that was sent:
56+
self.assertEqual(mail.outbox[0].to, ["[email protected]"])
57+
self.assertEqual(mail.outbox[0].tags, ["confirmation"]) # an Anymail custom attr
58+
59+
# Or verify the Anymail params, including any merged settings defaults:
60+
self.assertTrue(mail.outbox[0].anymail_test_params["track_clicks"])
61+
62+
Note that :data:`django.core.mail.outbox` is an "outbox," not an attempt to represent end users'
63+
*inboxes*. When using Django's default locmem EmailBackend, each outbox item represents a single
64+
call to an SMTP server. With Anymail's test EmailBackend, each outbox item represents a single
65+
call to an ESP's send API. (Anymail does not try to simulate how an ESP might further process
66+
the message for that API call: Anymail can't render :ref:`esp-stored-templates`, and it keeps a
67+
:ref:`batch send<batch-send>` message as a single outbox item, representing the single ESP API call
68+
that will send multiple messages. You can check ``outbox[n].anymail_test_params['is_batch_send']``
69+
to see if a message would fall under Anymail's batch send logic.)
70+
71+
72+
.. _testing-webhooks:
73+
.. _testing-tracking:
74+
75+
Testing tracking webhooks
76+
-------------------------
77+
78+
If you are using Anymail's :ref:`event tracking webhooks <event-tracking>`,
79+
you'll likely want to test your signal receiver code that processes those events.
80+
81+
One easy approach is to create a simulated :class:`~anymail.signals.AnymailTrackingEvent`
82+
in your test case, then call :func:`anymail.signals.tracking.send` to deliver it to your
83+
receiver function(s). Here's an example:
84+
85+
.. code-block:: python
86+
87+
from anymail.signals import AnymailTrackingEvent, tracking
88+
from django.test import TestCase
89+
90+
class EmailTrackingTests(TestCase):
91+
def test_delivered_event(self):
92+
# Build an AnymailTrackingEvent with event_type (required)
93+
# and any other attributes your receiver cares about. E.g.:
94+
event = AnymailTrackingEvent(
95+
event_type="delivered",
96+
recipient="[email protected]",
97+
message_id="test-message-id",
98+
)
99+
100+
# Invoke all registered Anymail tracking signal receivers:
101+
tracking.send(sender=object(), event=event, esp_name="TestESP")
102+
103+
# Verify expected behavior of your receiver. What to test here
104+
# depends on how your code handles the tracking events. E.g., if
105+
# you create a Django model to store the event, you might check:
106+
from myapp.models import MyTrackingModel
107+
self.assertTrue(MyTrackingModel.objects.filter(
108+
email="[email protected]", event="delivered",
109+
message_id="test-message-id",
110+
).exists())
111+
112+
def test_bounced_event(self):
113+
# ... as above, but with `event_type="bounced"`
114+
# etc.
115+
116+
This example uses Django's :meth:`Signal.send <django.dispatch.Signal.send>`,
117+
so the test also verifies your receiver was registered properly, and it will
118+
call multiple receiver functions if your code uses them.
119+
120+
Your test cases could instead import your tracking receiver function and call it
121+
directly with the simulated event data. (Either approach is effective, and which
122+
to use is largely a matter of personal taste.)
123+
124+
125+
.. _testing-inbound:
126+
.. _testing-receiving:
127+
128+
Testing receiving mail
129+
----------------------
130+
131+
If your project handles :ref:`receiving inbound mail <inbound>`, you can test that with
132+
an approach similar to the one used for event tracking webhooks above.
133+
134+
First build a simulated :class:`~anymail.signals.AnymailInboundEvent` containing
135+
a simulated :class:`~anymail.inbound.AnymailInboundMessage`. Then dispatch
136+
to your inbound receiver function(s) with :func:`anymail.signals.inbound.send`.
137+
Like this:
138+
139+
.. code-block:: python
140+
141+
from anymail.inbound import AnymailInboundMessage
142+
from anymail.signals import AnymailInboundEvent, inbound
143+
from django.test import TestCase
144+
145+
class EmailReceivingTests(TestCase):
146+
def test_inbound_event(self):
147+
# Build a simple AnymailInboundMessage and AnymailInboundEvent
148+
# (see tips for more complex messages after the example):
149+
message = AnymailInboundMessage.construct(
150+
151+
subject="subject", text="text body", html="html body")
152+
event = AnymailInboundEvent(message=message)
153+
154+
# Invoke all registered Anymail inbound signal receivers:
155+
inbound.send(sender=object(), event=event, esp_name="TestESP")
156+
157+
# Verify expected behavior of your receiver. What to test here
158+
# depends on how your code handles the inbound message. E.g., if
159+
# you create a user comment from the message, you might check:
160+
from myapp.models import MyCommentModel
161+
comment = MyCommentModel.objects.get(poster="[email protected]")
162+
self.assertEqual(comment.text, "text body")
163+
164+
For examples of various ways to build an :class:`~anymail.inbound.AnymailInboundMessage`,
165+
set headers, add attachments, etc., see `test_inbound.py`_ in Anymail's tests. In particular,
166+
you may find ``AnymailInboundMessage.parse_raw_mime(str)`` or
167+
``AnymailInboundMessage.parse_raw_mime_file(fp)`` useful for loading complex, real-world
168+
email messages into test cases.
169+
170+
.. _test_inbound.py:
171+
https://github.com/anymail/django-anymail/blob/main/tests/test_inbound.py

0 commit comments

Comments
 (0)