Mock smtplib to Test Patch Email Series in Python
Simulate sending a numbered series of patch emails with smtplib and verify the calls using unittest.mock without a real mail server.
Python code
35 linesimport smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from unittest.mock import patch, Mock
def send_patch_series(subject_prefix, patches, smtp_host="localhost", smtp_port=25):
"""Simulate sending a series of patch emails."""
for i, patch_content in enumerate(patches, start=1):
msg = MIMEMultipart()
msg["From"] = "sender@example.com"
msg["To"] = "recipient@example.com"
msg["Subject"] = f"{subject_prefix} [{i}/{len(patches)}]"
msg.attach(MIMEText(patch_content, "plain"))
with smtplib.SMTP(smtp_host, smtp_port) as server:
server.send_message(msg)
print(f"Sent: {msg['Subject']}")
def demo():
patches = [
"diff --git a/file1.py b/file1.py\n+def new_function():",
"diff --git a/file2.py b/file2.py\n+class NewClass:",
"diff --git a/file3.py b/file3.py\n+import new_module",
]
fake_server = Mock()
with patch("smtplib.SMTP", return_value=fake_server) as mock_smtp:
send_patch_series("PATCH: feature-x", patches)
print("\nSMTP called:", mock_smtp.call_count, "time(s)")
print("send_message calls:", fake_server.send_message.call_count)
if __name__ == "__main__":
demo()
Output
Sent: PATCH: feature-x [1/3]
Sent: PATCH: feature-x [2/3]
Sent: PATCH: feature-x [3/3]
SMTP called: 3 time(s)
send_message calls: 3
How it works
The patch("smtplib.SMTP", return_value=fake_server) context manager replaces the real SMTP constructor with a Mock object, so no network connection is made. Each loop iteration creates a MIMEMultipart message, attaches the patch diff as plain text, and opens a fake SMTP context through the mock. Calling server.send_message(msg) invokes the mock, which records the call for later assertions. The final prints confirm how many times SMTP was constructed and how many messages were sent, verifying the series was dispatched correctly.
Common mistakes
- Forgetting to patch the full path `smtplib.SMTP` instead of just `SMTP` in the local scope
- Not passing `return_value=fake_server`, which makes every context manager return a new Mock with no shared call count
- Assuming real emails are sent when the mock is active — assert on the fake server, not on SMTP side effects
Variations
- Use `unittest.mock.patch` as a decorator above the test function for cleaner separation
- Replace the Mock with `unittest.mock.MagicMock` to also handle async methods or nested calls if needed
Real-world use cases
- Testing a CI pipeline that emails patch series to maintainers without spamming a real mailbox.
- Validating email subject formatting and recipient logic in code-review bots sending change notifications.
- Simulating multi-email workflows in regression tests so local development needs no local SMTP server.
Sponsored
More from Git + Python
- Amend Last Commit Message in Python easy
- Bisect Good Bad Automation Script in Python easy
- Build a Simple Log Graph in Python easy
- Bump Semantic Version Git Tag in Python easy
- Count Unique Contributors from Git Shortlog in Python easy
- Create a Mock GitHub Release API in Python for Testing gh CLI easy
Keep learning
Related tutorials and quizzes for this topic.