Guides

Testing

Code that sends email should be easy to test. MailingKit's memory provider records every message instead of sending it, so tests stay fast, offline and deterministic.

The memory provider

test_signup.py
from mailingkit import MailingKit
from mailingkit.providers import MemoryProvider

async def test_signup_sends_welcome_email():
    outbox = MemoryProvider()
    mail = MailingKit(outbox, default_sender="app@example.com", templates="templates")

    await signup(mail, name="Ada", email="ada@example.com")

    assert outbox.last.subject == "Welcome to Acme, Ada"
    assert "Open your workspace" in outbox.last.text
    assert len(outbox.sent_to("ada@example.com")) == 1
MemberWhat it gives you
outboxEvery message sent, in order, as EmailMessage objects.
lastThe most recent message. Raises LookupError if none.
sent_to(email)Messages where the address is a to, cc or bcc recipient.
idempotency_keysThe key passed with each send.
fail_next(*errors)Raise these errors on the next sends, in order.
clear()Reset everything between tests.

A pytest fixture

conftest.py
import pytest
from mailingkit import MailingKit, RetryPolicy
from mailingkit.providers import MemoryProvider

@pytest.fixture
def outbox():
    return MemoryProvider()

@pytest.fixture
def mail(outbox):
    return MailingKit(
        outbox,
        default_sender="app@example.com",
        templates="templates",
        retry=RetryPolicy(initial_delay=0, jitter=False),  # retries without waiting
    )

Inject the client into your code (a function argument, a FastAPI dependency, a Django setting) rather than importing a global, and tests can swap it freely.

Testing failure paths

python
from mailingkit import AuthenticationError, TemporaryProviderError

async def test_transient_failure_is_retried(mail, outbox):
    outbox.fail_next(TemporaryProviderError("timeout"))

    result = await mail.send(to="ada@example.com", subject="Hi", text="Hello")

    assert result.attempts == 2

async def test_bad_credentials_surface_as_one_error(mail, outbox):
    outbox.fail_next(AuthenticationError("bad key"))

    with pytest.raises(AuthenticationError):
        await mail.send(to="ada@example.com", subject="Hi", text="Hello")

Testing templates

Render templates directly to check copy and data without any provider:

python
def test_receipt_shows_total(mail):
    rendered = mail.render("receipt", {"order_id": 7, "total": "$12.00"})

    assert rendered.subject == "Receipt for order 7"
    assert "$12.00" in rendered.html

Django tests

Point the backend at a client built on a memory provider in your test settings, then assert on the provider's outbox. Django's own locmem backend also still works if you prefer django.core.mail.outbox.