Missive provides first-class testing support with LocalMailer and assertion helpers.
Enable the local feature:
[dev-dependencies]
missive = { version = "0.7.0", features = ["local"] }use missive::{Email, EmailClient};
use missive::providers::LocalMailer;
use missive::testing::*;
#[tokio::test]
async fn test_sends_welcome_email() {
// Create test mailer
let mailer = LocalMailer::new();
let client = EmailClient::new(mailer.clone())
.with_default_from("noreply@example.com");
// Your code that sends email
let email = Email::new()
.to("user@example.com")
.subject("Welcome!")
.text_body("Thanks for signing up.");
client.deliver(email).await.unwrap();
// Assertions
assert_email_sent(&mailer);
assert_email_to(&mailer, "user@example.com");
assert_email_subject(&mailer, "Welcome!");
}| Function | Description |
|---|---|
assert_email_sent(&mailer) |
At least one email was sent |
assert_email_count(&mailer, n) |
Exactly n emails were sent |
assert_email_to(&mailer, email) |
Email sent to this address |
assert_email_from(&mailer, email) |
Last email was from this address |
assert_email_subject(&mailer, text) |
Email with exact subject exists |
assert_email_subject_contains(&mailer, text) |
Subject contains text |
assert_email_html_contains(&mailer, text) |
HTML body contains text |
assert_email_text_contains(&mailer, text) |
Text body contains text |
assert_email_has_attachment(&mailer, filename) |
Has attachment with filename |
assert_email_matches(&mailer, predicate) |
Custom predicate matches |
| Function | Description |
|---|---|
assert_no_emails_sent(&mailer) |
No emails were sent |
refute_email_to(&mailer, email) |
No email sent to this address |
refute_email_subject(&mailer, text) |
No email with this subject |
refute_email_matches(&mailer, predicate) |
No email matches predicate |
Requires the local feature (includes regex):
| Function | Description |
|---|---|
assert_email_subject_matches(&mailer, regex) |
Subject matches regex |
assert_email_html_matches(&mailer, regex) |
HTML body matches regex |
assert_email_text_matches(&mailer, regex) |
Text body matches regex |
Assertions provide detailed error messages showing actual emails:
Expected an email to be sent to 'wrong@example.com'.
Emails sent:
1. To: [user@example.com], From: sender@example.com, Subject: "Welcome!"
2. To: [admin@example.com], From: noreply@example.com, Subject: "Alert"
Use assert_email_matches for complex assertions:
assert_email_matches(&mailer, |email| {
email.to_addresses().iter().any(|a| a.email().ends_with("@company.com"))
&& email.subject_line().contains("Invoice")
&& !email.attachments().is_empty()
});Test error handling by configuring the mailer to fail:
#[tokio::test]
async fn test_handles_email_failure() {
let mailer = LocalMailer::new();
let client = EmailClient::new(mailer.clone())
.with_default_from("noreply@example.com");
// Configure to fail
mailer.set_failure("SMTP connection refused");
let email = Email::new()
.to("user@example.com")
.subject("Test");
let result = client.deliver(email).await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("connection refused"));
// Clear failure for subsequent tests
mailer.clear_failure();
}Atomically get and clear all emails:
#[tokio::test]
async fn test_multiple_phases() {
let mailer = LocalMailer::new();
// Phase 1: User signup
send_signup_emails(&mailer).await;
let signup_emails = flush_emails(&mailer);
assert_eq!(signup_emails.len(), 2); // welcome + verification
// Mailer is now empty
assert_no_emails_sent(&mailer);
// Phase 2: Password reset
send_password_reset(&mailer).await;
assert_email_count(&mailer, 1);
assert_email_subject_contains(&mailer, "Password");
}Access the raw Email struct for detailed inspection:
let mailer = LocalMailer::new();
// ... send emails ...
// Get all emails
let emails = mailer.emails();
// Get most recent email
if let Some(stored) = mailer.last_email() {
let email = &stored.email;
println!("To: {:?}", email.to_addresses());
println!("Subject: {}", email.subject_line());
println!("HTML: {:?}", email.html_body_content());
println!("Attachments: {}", email.attachments().len());
}
// Find specific emails
let welcome_emails = mailer.find_emails(|e| e.subject_line().contains("Welcome"));#[tokio::test]
async fn test_email() {
let mailer = LocalMailer::new();
// ...
}fn test_client() -> (EmailClient<LocalMailer>, LocalMailer) {
let mailer = LocalMailer::new();
let client = EmailClient::new(mailer.clone())
.with_default_from("noreply@example.com");
(client, mailer)
}
#[tokio::test]
async fn test_one() {
let (client, mailer) = test_client();
// ...
}Each LocalMailer::new() creates fresh storage, so tests are isolated by default. If sharing a mailer:
mailer.clear(); // Remove all captured emails- Use
EmailClient- Pass the test client explicitly for clarity - Assert specifics - Check subject, recipients, not just "email sent"
- Test error paths - Use
set_failure()to test error handling - Use
flush_emails- When testing multi-step flows - Check attachments - Verify attachments are included when expected