In many web-based application the process of registration includes a step of email verification. The process is more or less this:
- The user types in an email address (and some other date)
- The system checks that the looks ok, including the format of the email address. (e.g. that it is not empty)
- Then it generates a unique code and stores all the received data and the unique code in the database.
- Then it send an email to the given address containing a URL with the unique code.
- When the user receives the email and clicks on the link the web system receives this verification request and can update the records marking the email as verified.
There can and probably should be a number of extra things in the process.
- For example the database should contain an expiration date for the code.
The question though, how do you test this code? Do you let the system send out real emails on every run? That would be slow and the test would need to rely an external service that will receive the email from where the test can download it for verification.
One solution for this is to save the email message in a file during the tests and make the tests read that file.
This is what you can see in this similified example.
Cargo.toml
examples/testing-email-sending/Cargo.toml
[package]
name = "mail-sender"
version = "0.1.0"
edition = "2024"
[dependencies]
anyhow = "1.0.100"
clap = { version = "4.5.53", features = ["derive"] }
lettre = { version = "0.11.19", features = ["file-transport", "sendmail-transport"] }
uuid = "1.19.0"
main.rs
examples/testing-email-sending/src/main.rs
use clap::Parser;
use lettre::{FileTransport, Message, SendmailTransport, Transport, message::header::ContentType};
#[derive(Parser)]
struct Cli {
#[arg(long)]
from: String,
#[arg(long)]
to: String,
}
fn main() {
let args = Cli::parse();
// We get this from the command line.
// In a real application it would come from a config file.
let from = &args.from;
// This is what the user entered when registering.
let user_email = args.to.clone();
register(from, &user_email);
}
fn register(admin_email: &str, user_email: &str) -> String {
// This code is saved to the database and emailed to the user.
let code = uuid::Uuid::new_v4().to_string();
let subject = String::from("Email sent to confirm registration");
let body = format!("Welcome! Use this code {code} to verify your email address.");
let email = Message::builder()
.from(admin_email.parse().unwrap())
.to(user_email.parse().unwrap())
.subject(subject)
.header(ContentType::TEXT_PLAIN)
.body(body)
.unwrap();
if cfg!(test) {
let dirname = ".";
let sender = FileTransport::new(dirname);
let filename = sender.send(&email).unwrap();
format!("{filename}.eml")
} else {
println!("Sending email to {}", user_email);
let sender = SendmailTransport::new();
sender.send(&email).unwrap();
String::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_register_creates_valid_email() {
let filename = register("admin@example.com", "user@example.com");
let content = std::fs::read_to_string(filename).unwrap();
assert!(content.contains("From: admin@example.com"));
assert!(content.contains("To: user@example.com"));
assert!(content.contains("Subject: Email sent to confirm registration"));
assert!(content.contains("Welcome! Use this code"));
let code_start =
content.find("Welcome! Use this code ").unwrap() + "Welcome! Use this code ".len();
let code_end = content.find(" to verify your").unwrap();
let _code = &content[code_start..code_end];
// We can then use the code to verify the user in the test.
}
}
Config test
We can either use
#[cfg(test)]
and
#[cfg(not(test))]
or we can use
if cfg!(test) {
...
} else {
...
}