How to Set Up a Fake SMTP Server for Local and CI Tests
Run smtp4dev locally, send a sample email, and assert its recipient, subject and body with a downloadable Python example for local and CI tests.
A fake SMTP server captures email from your application so you can inspect it during development. That gives you somewhere to send a signup, receipt or password-reset email without using a customer's mailbox. The next step is turning that capture into a test: did this run produce a new message with the recipient and content you expected?
This example uses smtp4dev and Python to send a synthetic message, find its capture and check its contents. The supplied configuration has no upstream relay configured. For inbox APIs, webhooks and a broader choice of testing approaches, see the fake SMTP server guide.
Quick setup model
You'll need Docker running Linux containers, Docker Compose and Python 3.9 or newer. The Python scripts use the standard library, so there are no packages to install or email credentials to supply.
Download the complete fake SMTP example and extract it. The fake-smtp-testing folder contains the Compose configuration, send_and_assert.py, the failure checks and a README. Run the following commands from that folder.
The example pins smtp4dev 3.15.0 and its Docker image digest. It publishes SMTP on 127.0.0.1:2525 and the web interface on 127.0.0.1:8025, leaves the relay unset and locks settings. Keep those loopback bindings and do not add a production relay to this setup; smtp4dev's Docker security guidance explains the exposure risk of publishing its ports more broadly.
Example local setup with smtp4dev
Start the capture server:
docker compose -p fake-smtp-example up -d
If your installation uses the standalone Compose command, use docker-compose in place of docker compose. Wait for the web interface to open at http://127.0.0.1:8025, then send and check the sample:
python3 send_and_assert.py
A successful run prints:
PASS: new captured recipient, subject, Message-ID, sender and body match
The message uses a new synthetic recipient under example.test, a unique subject and a matching Message-ID. Open the capture in the web interface to inspect the same message. The script also downloads its RFC 822 source and checks the sender, recipient headers and plain-text body; seeing a row in the inbox is only the start of the assertion.
Point your application at the capture server
For an application running directly on your computer, use these development settings:
| Setting | Value |
|---|---|
| SMTP host | 127.0.0.1 |
| SMTP port | 2525 |
| SMTP authentication | Disabled |
| SMTP TLS | Disabled for this local capture setup |
Trigger the application action that sends an email, such as creating a synthetic test account. When adapting the Python example, replace its send_message call with that action and update the expected content. Take the snapshot of existing captures before triggering the application.
An application container attached to the same Compose capture network uses host smtp4dev and port 25. An assertion script in that network uses API URL http://smtp4dev:80. Inside a container, 127.0.0.1 refers to that container itself; the published 2525 and 8025 ports are for commands running on your host.
What to validate in fake SMTP tests
A test that accepts the newest email can pass because of an old message or another test running at the same time. The sample generates a unique token, excludes captures already present before the send and requires an exact recipient and subject match.
It polls GET /api/messages using that token. In smtp4dev 3.15.0, this endpoint returns a paginated object with a results array. The script follows the pages, then fetches GET /api/messages/{id}/download to parse the captured message. You can inspect the running server's API documentation for the response fields.
Run the included failure checks while smtp4dev is running:
python3 test_failures.py --integration
The nine checks cover the successful content assertion and failures involving the wrong subject, wrong recipient, stale capture, missing message and wrong body. The tests pass when those bad cases are rejected. To run only the five checks that do not need a server, omit --integration.
send_and_assert.py exits with a nonzero status when its assertions fail or its wait times out. Its default wait is ten seconds; change it with --timeout to suit your application's test environment. If the API connection is refused, check docker compose -p fake-smtp-example logs smtp4dev and wait for startup before retrying.
The sample checks a plain-text email. Extend it for your templates: assert reset-link destinations, required HTML and text parts, attachment names and content types, or the expected number of messages after a retry. Those additional checks belong in your application's tests; they are not covered by the sample's nine checks.
Why not test with personal Gmail inboxes?
A personal mailbox mixes test messages with ordinary mail and ties the test to someone's account. Repeated manual searches also make it easy to inspect a message from the wrong run. Use the isolated capture for quick development checks, with unique recipients and content assertions.
Controlled tests against a real mailbox provider answer a different question: what happens after mail leaves your sending system? Keep those checks separate so a local content failure and an external delivery problem are easy to distinguish.
Local fake SMTP limitations
The successful sample proves that smtp4dev accepted the message and that its captured content matched the assertions. It does not test delivery to Gmail or Outlook, spam placement, or rendering in an email client.
For those checks, use the intended sending path and controlled test recipients. MailSlurp's email integration testing supports receive-side assertions, while an email deliverability test helps you investigate delivery and authentication separately. A browser view of captured HTML is not evidence of how every email client will display it.
Local to CI to production workflow
In CI, start a fresh capture service for the job, wait for its API to become available, run the application action and fail the job when the message assertion fails. Keep a unique recipient or token per test, including parallel tests. Use the host or container addresses above according to where the test runner executes.
On a dedicated runner using the example's default ports, the same two Python commands exercise the successful capture and failure checks. A shared runner may need different published ports: update compose.yaml and pass matching --smtp-port and --api-url options to send_and_assert.py. The supplied integration checks use the default ports.
| Stage | What to check | Where to continue |
|---|---|---|
| Local development | SMTP submission and the generated message's content | This smtp4dev example |
| Automated application tests | A new email from the expected app action, with the correct recipient, links and content | Email integration testing |
| Pre-release delivery tests | The configured sending path, authentication and controlled mailbox results | Email Sandbox and deliverability testing |
When finished, remove this example's container, network and captured-message volume:
docker compose -p fake-smtp-example down --volumes
This deletes the example's captures. Save any messages you need before cleanup, and run the equivalent cleanup step after a CI job even if a test fails.
Release checklist
Before relying on an email test, check that it:
- Uses development SMTP settings and synthetic recipients, with no production relay configured.
- Triggers the application action you intend to verify, rather than only the sample sender.
- Rejects stale or unrelated messages and fails when no matching capture arrives.
- Checks the content your customers need, including links or attachments where applicable.
- Keeps external delivery, SPF/DKIM/DMARC and provider-event checks separate from local capture assertions.
For protocol troubleshooting, see SMTP commands and responses. When your sending provider exposes delivery, bounce or reject events, test that event path too; the Email Webhooks guide covers webhook-based checks.
FAQ
Is a fake SMTP server enough for production confidence?
It verifies part of the journey: your app submits a message and the capture contains the expected data. Add tests through your intended sending service and controlled mailboxes for delivery, authentication, provider events and client rendering.
Which fake SMTP tool should I choose?
This example uses smtp4dev because the web interface lets you inspect messages and its API lets the Python script assert them. The SMTP4dev guide covers the tool in more detail; the MailHog tutorial provides another local capture workflow.
Can I run fake SMTP in CI?
Yes. Run the capture service alongside your application and use API assertions with a timeout. Give each test a unique message identifier and make assertion failures fail the job. Keep the service isolated to the test environment.
Next steps
Use the fake SMTP server guide to compare local capture with programmable inbox testing, or the fake SMTP explainer for the underlying concepts. For checks beyond message capture, see email testing tools and email deliverability testing.