Skip to content

Latest commit

 

History

History
225 lines (166 loc) · 5.89 KB

File metadata and controls

225 lines (166 loc) · 5.89 KB

Testing

Missive provides first-class testing support with LocalMailer and assertion helpers.

Setup

Enable the local feature:

[dev-dependencies]
missive = { version = "0.7.0", features = ["local"] }

Basic Usage

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!");
}

Available Assertions

Positive Assertions

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

Negative Assertions

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

Regex Assertions

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

Error Messages

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"

Custom Predicates

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()
});

Simulating Failures

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();
}

Flush Emails

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");
}

Inspecting Emails

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"));

Integration with Test Frameworks

With tokio::test

#[tokio::test]
async fn test_email() {
    let mailer = LocalMailer::new();
    // ...
}

Shared Test Fixtures

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();
    // ...
}

Clearing Between Tests

Each LocalMailer::new() creates fresh storage, so tests are isolated by default. If sharing a mailer:

mailer.clear();  // Remove all captured emails

Best Practices

  1. Use EmailClient - Pass the test client explicitly for clarity
  2. Assert specifics - Check subject, recipients, not just "email sent"
  3. Test error paths - Use set_failure() to test error handling
  4. Use flush_emails - When testing multi-step flows
  5. Check attachments - Verify attachments are included when expected