Technical Article

Testing Email Workflows Without Sending Real Emails

Email workflows are notoriously easy to test poorly.

The standard pattern is all too familiar: mock the email transport client, assert that send() was invoked with some parameters, and ship the feature. However, this test only proves that your code attempted to execute a function call—it does not prove that the underlying workflow actually functions as intended.

Testing the Complete Asynchronous Path

A far more reliable strategy tests the full asynchronous path inside a production-like local or containerized test environment:

  1. The application triggers a domain event.
  2. A background job is queued with an explicit payload contract.
  3. The queue worker consumes the job and loads required domain state.
  4. The email template renders using real data, formatting, and localization strings.
  5. The email client delivers the message to a local, isolated SMTP sink such as Mailpit.
  6. An automated test inspects the inbox via API or browser automation to assert on headers, links, and content.

Failures That Mocking Inevitably Misses

Relying strictly on unit-level mocks creates a false sense of security while letting critical integration bugs slip through:

  • Missing template variables: Placeholders that render as undefined or crash during interpolation.
  • Broken action links: Malformed query parameters, expired tokens, or invalid base URLs.
  • Incorrect localization: Delivering strings in the default language instead of the user's preferred locale.
  • Duplicate deliveries: Missing idempotency keys resulting in multiple messages sent for a single event.
  • Worker serialization errors: Failures when transforming dates or custom data structures across queue boundaries.
  • Malformed HTML or text rendering: Layout glitches that break email client compatibility.

The End-to-End Pipeline

By pairing containerized services—such as a real database, Redis, background workers, and Mailpit—your tests evaluate the exact pipeline your users experience:

Event → Queue → Worker → Template → SMTP → Inbox

Unit tests and mocks still have their place for testing isolated conditional branches quickly. But for multi-step workflows with asynchronous side effects, a single end-to-end test with a local SMTP sink catches an entire category of integration bugs before they reach production.

The objective is not to send real emails in tests; it is to verify the real email workflow safely and deterministically.