File: testing.rst

package info (click to toggle)
django-anymail 13.1-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 2,552 kB
  • sloc: python: 28,882; makefile: 132; javascript: 33; sh: 9
file content (171 lines) | stat: -rw-r--r-- 7,692 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
.. _testing:

Testing your app
================

.. _test-backend:
.. _testing-sending:

Testing sending mail
--------------------

Django's documentation covers the basics of
:ref:`testing email sending in Django <django:topics-testing-email>`.
Everything in their examples will work with projects using Anymail.

Django's test runner makes sure your test cases don't actually send email,
by loading a dummy "locmem" EmailBackend that accumulates messages
in memory rather than sending them. You may not need anything more
complicated for verifying your app.

Anymail also includes its own "test" EmailBackend. This is intended primarily for
Anymail's internal testing, 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.

* 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_test_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_test_params["track_clicks"])

Note that :data:`django.core.mail.outbox` is an "outbox," not an attempt to represent end users'
*inboxes*. When using Django's default locmem EmailBackend, each outbox item represents a single
call to an SMTP server. With Anymail's test EmailBackend, each outbox item represents a single
call to an ESP's send API. (Anymail does not try to simulate how an ESP might further process
the message for that API call: Anymail can't render :ref:`esp-stored-templates`, and it keeps a
:ref:`batch send<batch-send>` message as a single outbox item, representing the single ESP API call
that will send multiple messages. You can check ``outbox[n].anymail_test_params['is_batch_send']``
to see if a message would fall under Anymail's batch send logic.)


.. _testing-webhooks:
.. _testing-tracking:

Testing tracking webhooks
-------------------------

If you are using Anymail's :ref:`event tracking webhooks <event-tracking>`,
you'll likely want to test your signal receiver code that processes those events.

One easy approach is to create a simulated :class:`~anymail.signals.AnymailTrackingEvent`
in your test case, then call :func:`anymail.signals.tracking.send` to deliver it to your
receiver function(s). Here's an example:

.. code-block:: python

    from anymail.signals import AnymailTrackingEvent, tracking
    from django.test import TestCase

    class EmailTrackingTests(TestCase):
        def test_delivered_event(self):
            # Build an AnymailTrackingEvent with event_type (required)
            # and any other attributes your receiver cares about. E.g.:
            event = AnymailTrackingEvent(
                event_type="delivered",
                recipient="to@example.com",
                message_id="test-message-id",
            )

            # Invoke all registered Anymail tracking signal receivers:
            tracking.send(sender=object(), event=event, esp_name="TestESP")

            # Verify expected behavior of your receiver. What to test here
            # depends on how your code handles the tracking events. E.g., if
            # you create a Django model to store the event, you might check:
            from myapp.models import MyTrackingModel
            self.assertTrue(MyTrackingModel.objects.filter(
                email="to@example.com", event="delivered",
                message_id="test-message-id",
            ).exists())

        def test_bounced_event(self):
            # ... as above, but with `event_type="bounced"`
            # etc.

This example uses Django's :meth:`Signal.send <django.dispatch.Signal.send>`,
so the test also verifies your receiver was registered properly, and it will
call multiple receiver functions if your code uses them.

Your test cases could instead import your tracking receiver function and call it
directly with the simulated event data. (Either approach is effective, and which
to use is largely a matter of personal taste.)


.. _testing-inbound:
.. _testing-receiving:

Testing receiving mail
----------------------

If your project handles :ref:`receiving inbound mail <inbound>`, you can test that with
an approach similar to the one used for event tracking webhooks above.

First build a simulated :class:`~anymail.signals.AnymailInboundEvent` containing
a simulated :class:`~anymail.inbound.AnymailInboundMessage`. Then dispatch
to your inbound receiver function(s) with :func:`anymail.signals.inbound.send`.
Like this:

.. code-block:: python

    from anymail.inbound import AnymailInboundMessage
    from anymail.signals import AnymailInboundEvent, inbound
    from django.test import TestCase

    class EmailReceivingTests(TestCase):
        def test_inbound_event(self):
            # Build a simple AnymailInboundMessage and AnymailInboundEvent
            # (see tips for more complex messages after the example):
            message = AnymailInboundMessage.construct(
                from_email="user@example.com", to="comments@example.net",
                subject="subject", text="text body", html="html body")
            event = AnymailInboundEvent(message=message)

            # Invoke all registered Anymail inbound signal receivers:
            inbound.send(sender=object(), event=event, esp_name="TestESP")

            # Verify expected behavior of your receiver. What to test here
            # depends on how your code handles the inbound message. E.g., if
            # you create a user comment from the message, you might check:
            from myapp.models import MyCommentModel
            comment = MyCommentModel.objects.get(poster="user@example.com")
            self.assertEqual(comment.text, "text body")

For examples of various ways to build an :class:`~anymail.inbound.AnymailInboundMessage`,
set headers, add attachments, etc., see `test_inbound.py`_ in Anymail's tests. In particular,
you may find ``AnymailInboundMessage.parse_raw_mime(str)`` or
``AnymailInboundMessage.parse_raw_mime_file(fp)`` useful for loading complex, real-world
email messages into test cases.

.. _test_inbound.py:
  https://github.com/anymail/django-anymail/blob/main/tests/test_inbound.py