Unit tests live in the same file as the code being tested, in a #[cfg(test)] module:
src/commands/ping.rs
// ── Tests ───────────────────────────────────────────────────────────────#[cfg(test)]mod tests { use super::*; #[test] fn ping_happy_path_returns_message_with_greeting() { let response = handle_ping("<@123456>"); assert!(response.is_message(), "expected Message variant"); let text = response.unwrap_message(); assert!( text.contains("<@123456>"), "response should mention the user" ); assert!( text.contains("Annie Mei"), "response should mention the bot name" ); } #[test] fn ping_response_includes_bot_description() { let text = handle_ping("<@999>").unwrap_message(); assert!( text.contains("anime and manga"), "response should describe what the bot does" ); }}
See src/commands/ping.rs:50-78 for the complete implementation.
Annie Mei follows a testable architecture pattern:
Core logic - Transport-agnostic, pure functions
Adapter - Thin wrapper that calls Serenity APIs
Example from src/commands/ping.rs:
// ── Core logic (transport-agnostic) ─────────────────────────────────────/// Produce the `/ping` response for the given user mention string.////// This is the testable entry-point — it never touches `Context` or/// `CommandInteraction`.pub fn handle_ping(user_mention: &str) -> CommandResponse { CommandResponse::Message(format!( "Hello {user_mention}! I'm Annie Mei, a bot that helps you find anime and manga!", ))}// ── Serenity adapter (thin wrapper) ─────────────────────────────────────pub async fn run(ctx: &Context, interaction: &CommandInteraction) { let user = &interaction.user; configure_sentry_scope("Ping", user.id.get(), None); let reply = handle_ping(&user.mention().to_string()); // Send response via Serenity...}
This separation allows testing handle_ping() without mocking Discord APIs.
use annie_mei::commands::ping::handle_ping;use annie_mei::commands::response::CommandResponse;#[test]fn test_ping_integration() { let response = handle_ping("<@12345>"); assert!(response.is_message());}