# Improve email unit-testing

**URL:** <https://forum.djangoproject.com/t/improve-email-unit-testing/32044>\
**Category:** Django Internals\
**Created:** [June 13, 2024, 9:08am UTC](https://forum.djangoproject.com/t/improve-email-unit-testing/32044 "2024-06-13T09:08:48Z")\
**Posts on this page:** 13\
**Page:** 1

<div class="post-metadata">

**Author:** ![GitRon](https://sea2.discourse-cdn.com/flex026/user_avatar/forum.djangoproject.com/gitron/32/13088_2.png) [@GitRon](https://forum.djangoproject.com/u/GitRon)\
**Post date:** [June 13, 2024, 9:08am UTC](https://forum.djangoproject.com/t/improve-email-unit-testing/32044/1 "2024-06-13T09:08:48Z")

</div>

Hello Django-world!

As some might have seen in Copenhagen (Day) or Vigo, I’ve created a package called django-pony-express. Since I got (in my opinion) really good feedback from a bunch of smart people, I’d like to suggest to move some stuff that I’ve built into core.

I think, the test suite might be a great starting point. I’ve create a wrapper for `mail.outbox` so you can use it like a Django Queryset. ([Docs](https://django-pony-express.readthedocs.io/en/latest/features/tests.html)).

Current:  
`html_content = mail.outbox[0].alternatives[0][0]`

My suggestion:  
`html_content = email_test_service.filter(subject='Nigerian prince').get_html_content()`

And for assertions:

```auto
list_of_emails = email_test_service.filter(subject=subject)

# We expect an email to be sent
self.assertEqual(list_of_emails.count(), previous_email_count + 1)

# Assert content
list_of_emails.assert_body_contains(f"Hola {user.first_name}")

```

Currently, the functionality lives in a class which you have to instanciate. I’m open to suggestions on how to make this (even) more django-esque.

The code is simple, it works, it’s documented and it’s tested.

I’d like go gather some thumbs-up to check if this is worth following up on.

Best from Cologne  
Ronny

---

<div class="post-metadata">

**Author:** ![sarahboyce](https://sea2.discourse-cdn.com/flex026/user_avatar/forum.djangoproject.com/sarahboyce/32/12734_2.png) [@sarahboyce](https://forum.djangoproject.com/u/sarahboyce)\
**Post date:** [June 14, 2024, 8:01am UTC](https://forum.djangoproject.com/t/improve-email-unit-testing/32044/2 "2024-06-14T08:01:52Z")

</div>

Hello 👋 thank you for raising this discussion

I am +1 on having `get_html_content()` or a `html_content` attribute on [`EmailMessage`](https://docs.djangoproject.com/en/5.0/topics/email/#django.core.mail.EmailMessage), because `mail.outbox[0].alternatives[0][0]` is not intuitive that this is html content and emails having html content is common.

On some of the helper asserts (such as `assert_body_contains` which asserts this is both in the text and html content), my main question is on the API. For example:

```python
# Your proposal
list_of_emails.assert_body_contains(f"Hola {user.first_name}")

# Alternative?
test_email = mail.outbox[0]
self.assertEmailBodyContains(test_email, f"Hola {user.first_name}")

```

I think once we get the API down and make sure we have good error messages and docs for these I would be +1 on adding a helper like this as having content in both the text and html part is good practice and a nice battery to add. However, I think we need more voices on what the right API here would be.

On `.filter()`, I can see that this looks nice but I’m wondering if people will want `.get()` and `.exists()` and others if we go down that road. I am -0 currently. Not sure the value gained is worth the effort 🤔

* * *

**Note:** we should extend the [email service testing topic](https://docs.djangoproject.com/en/5.0/topics/testing/tools/#email-services) in Django to include an email with html and the asserts you will want to do here. This can then be updated with `get_html_content()` / `html_content` and other helpers when they land.

---

<div class="post-metadata">

**Author:** ![sarahboyce](https://sea2.discourse-cdn.com/flex026/user_avatar/forum.djangoproject.com/sarahboyce/32/12734_2.png) [@sarahboyce](https://forum.djangoproject.com/u/sarahboyce)\
**Post date:** [June 14, 2024, 2:08pm UTC](https://forum.djangoproject.com/t/improve-email-unit-testing/32044/3 "2024-06-14T14:08:49Z")

</div>

**Update:**

- @GitRon has raised a PR on testing `EmailMultiAlternatives` messages: [Added docs about testing HTML email content · Pull Request #18273](https://github.com/django/django/pull/18273/)
- This has highlighted that @theorangeone has opened a PR recently with some overlap/similar goals: [Use a namedtuple for email attachments and alternatives · Pull Request #18261](https://github.com/django/django/pull/18261)

---

<div class="post-metadata">

**Author:** ![carltongibson](https://sea2.discourse-cdn.com/flex026/user_avatar/forum.djangoproject.com/carltongibson/32/267_2.png) [@carltongibson](https://forum.djangoproject.com/u/carltongibson)\
**Post date:** [June 15, 2024, 6:53am UTC](https://forum.djangoproject.com/t/improve-email-unit-testing/32044/4 "2024-06-15T06:53:47Z")

</div>

I’d like to add a warm emote in the general direction here. Yes, +1, let’s improve email testing. 🥰

> [@sarahboyce](#):
>
> `assertEmailBodyContains`

That way is _probably_ more in line with what we have. If we go this route, I wonder if a specialist `EmailTestCase` class would be worth it? 🤔 (Pro: keep email related methods in one place, separate from an already long list of methods. Con: do you end up need `TestCase` and friends anyway to write the actual tests you need? IDK 🤷 but it’s a thought.)

I not sure though that @GitRon’s suggestion of having the assertions on the helper isn’t the way forward… 🤔 — It’s a wrapper about the mailbox functionality, only used in tests, and then the filtering and assertion logic lives in the one place, not spread between the new wrapper and the test case.

It’s not the _helper’s_ responsibility to make assertions though. (That’s the tests). So rather than `assert_...` have methods that return a boolean – `.body_contains(...)` in the example – and then assert as usual in the test case – either `self.assertTrue(list_of_emails.body_contains(...))` for `unittest` based tests, or `assert list_of_emails.body_contains(...)` for those folks using `pytest`.  
(Pro there is keeping the _email_ related stuff in the helper class.)

(So many possibilities: we shouldn’t design it too much in committee — any of the options will be fine. 😅)

---

<div class="post-metadata">

**Author:** ![sarahboyce](https://sea2.discourse-cdn.com/flex026/user_avatar/forum.djangoproject.com/sarahboyce/32/12734_2.png) [@sarahboyce](https://forum.djangoproject.com/u/sarahboyce)\
**Post date:** [June 15, 2024, 1:02pm UTC](https://forum.djangoproject.com/t/improve-email-unit-testing/32044/5 "2024-06-15T13:02:24Z")

</div>

I like `.body_contains(...)` 👍

I’ve had a quick look into [`multipart/alternative` emails](https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html) as I was worried that emails might have multiple `"text/html"` alternatives. Apparently the order of attachments is important as they should increase in content “richness”/complexity.  
Maybe we should have `.alternative_body_contains(..., type=None)` which checks the last alternative or the last alternative of a particular type (e.g. `type="text/html"`). 🤷‍♀️

Edit: `type` is a bad name but you get the idea 😁

---

<div class="post-metadata">

**Author:** ![carltongibson](https://sea2.discourse-cdn.com/flex026/user_avatar/forum.djangoproject.com/carltongibson/32/267_2.png) [@carltongibson](https://forum.djangoproject.com/u/carltongibson)\
**Post date:** [June 15, 2024, 1:07pm UTC](https://forum.djangoproject.com/t/improve-email-unit-testing/32044/6 "2024-06-15T13:07:44Z")

</div>

One of the nice things @GitRon added was having the asserts check all the parts, so that you don’t forget to update just one template. Keeping something of that would be good. (Though there doesn’t have to be just one method, of course)

---

<div class="post-metadata">

**Author:** ![sarahboyce](https://sea2.discourse-cdn.com/flex026/user_avatar/forum.djangoproject.com/sarahboyce/32/12734_2.png) [@sarahboyce](https://forum.djangoproject.com/u/sarahboyce)\
**Post date:** [June 15, 2024, 3:02pm UTC](https://forum.djangoproject.com/t/improve-email-unit-testing/32044/7 "2024-06-15T15:02:53Z")

</div>

Agreed 👍 it’s a nice addition and helps people remember these parts should contain equivalent information

---

<div class="post-metadata">

**Author:** ![adamchainz](https://sea2.discourse-cdn.com/flex026/user_avatar/forum.djangoproject.com/adamchainz/32/26_2.png) [@adamchainz](https://forum.djangoproject.com/u/adamchainz)\
**Post date:** [June 15, 2024, 3:33pm UTC](https://forum.djangoproject.com/t/improve-email-unit-testing/32044/9 "2024-06-15T15:33:14Z")

</div>

Another big +1 from me to improving the API.

Jake’s PR looks like a good start.

A `body_contains()` method looks like a good compromise for making it possible to assert on multiple bodies without repetition.

> [@sarahboyce](#):
>
> On `.filter()`, I can see that this looks nice but I’m wondering if people will want `.get()` and `.exists()` and others if we go down that road. I am -0 currently. Not sure the value gained is worth the effort 🤔

I would be a -1 here. Python already provides many tools for filtering and sorting lists (list comprehensions, `sorted()`, `map()`, `filter()`). Reimplementing the QuerySet API would just be “another” way to do it, perhaps even an unintuitive one because there would be no database query or laziness.

---

<div class="post-metadata">

**Author:** ![GitRon](https://sea2.discourse-cdn.com/flex026/user_avatar/forum.djangoproject.com/gitron/32/13088_2.png) [@GitRon](https://forum.djangoproject.com/u/GitRon)\
**Post date:** [June 16, 2024, 3:14pm UTC](https://forum.djangoproject.com/t/improve-email-unit-testing/32044/10 "2024-06-16T15:14:29Z")

</div>

Thanks all for your feedback!

> [@adamchainz](#):
>
> A `body_contains()` method looks like a good compromise for making it possible to assert on multiple bodies without repetition.

Since it seems the consensus points towards this method, in which class would we put this helper? Just want to make sure that I’ll work in the right direction 😊

Additionally, how would we support other types like… audio that @sarahboyce mentioned. Just document for now that we just check plain text and HTML?

---

<div class="post-metadata">

**Author:** ![sarahboyce](https://sea2.discourse-cdn.com/flex026/user_avatar/forum.djangoproject.com/sarahboyce/32/12734_2.png) [@sarahboyce](https://forum.djangoproject.com/u/sarahboyce)\
**Post date:** [June 16, 2024, 5:59pm UTC](https://forum.djangoproject.com/t/improve-email-unit-testing/32044/11 "2024-06-16T17:59:29Z")

</div>

> Since it seems the consensus points towards this method, in which class would we put this helper?

Maybe on `EmailMultiAlternatives`?

On handling other types, I think we should only check text (so `text/plain`, `text/enriched`, `text/html` etc) and skip anything else.  
It may not even be a problem 🤔 but think it’s a good idea to check the MIME type

---

<div class="post-metadata">

**Author:** ![GitRon](https://sea2.discourse-cdn.com/flex026/user_avatar/forum.djangoproject.com/gitron/32/13088_2.png) [@GitRon](https://forum.djangoproject.com/u/GitRon)\
**Post date:** [June 17, 2024, 9:39am UTC](https://forum.djangoproject.com/t/improve-email-unit-testing/32044/12 "2024-06-17T09:39:45Z")

</div>

I’ve created a PR to add the suggestion: [Added "body\_contains" method to "EmailMultiAlternatives" by GitRon · Pull Request #18278 · django/django · GitHub](https://github.com/django/django/pull/18278)

Happy to get some feedback (it’s my first core-code contribution 😅)

---

<div class="post-metadata">

**Author:** ![GitRon](https://sea2.discourse-cdn.com/flex026/user_avatar/forum.djangoproject.com/gitron/32/13088_2.png) [@GitRon](https://forum.djangoproject.com/u/GitRon)\
**Post date:** [June 19, 2024, 11:55am UTC](https://forum.djangoproject.com/t/improve-email-unit-testing/32044/13 "2024-06-19T11:55:56Z")

</div>

Ok, so @theorangeone has a further improvement up his sleeve, my improvement to the emailalternatives class and the docs are somewhere on their way to core.

I do feel that some kind of wrapper for the mail.outbox would add genuine value but I can understand @adamchainz argumentation of reinventing the wheel.

So, the question is: Are we done here or do we want to add more improvements apart from the three PRs?

---

<div class="post-metadata">

**Author:** ![sarahboyce](https://sea2.discourse-cdn.com/flex026/user_avatar/forum.djangoproject.com/sarahboyce/32/12734_2.png) [@sarahboyce](https://forum.djangoproject.com/u/sarahboyce)\
**Post date:** [June 20, 2024, 9:30am UTC](https://forum.djangoproject.com/t/improve-email-unit-testing/32044/14 "2024-06-20T09:30:14Z")

</div>

> Ok, so @theorangeone has a further improvement up his sleeve…

Merged [Fixed #35537 – Changed EmailMessage.attachments and EmailMultiAltern… · django/django@aba0e54 (github.com)](https://github.com/django/django/commit/aba0e541caaa086f183197eaaca0ac20a730bbe4) ✅

> …my improvement to the emailalternatives class and the docs are somewhere on their way to core.

Hoping to merge [Fixed #35528 – Added helper method EmailMultiAlternatives.body\_contains(). by GitRon · Pull Request #18278 · django/django (github.com)](https://github.com/django/django/pull/18278/commits/13e056b2f79b578636c79b6c762a2d18dfed7207) soon 👍

> So, the question is: Are we done here or do we want to add more improvements apart from the three PRs?

Personally I this is a good place to stop for now.  
See if there is any feedback after the 5.2 release etc.
