|
| 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 | + |
| 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