From 2fa41b66e6bd0286b0ba2f2000a22d7403303012 Mon Sep 17 00:00:00 2001 From: kangalioo Date: Sun, 9 Apr 2023 00:31:38 +0200 Subject: [PATCH] Add unit test example Fixes #142 --- src/lib.rs | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 1779e4904558..ea6c156703bd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -310,6 +310,70 @@ serenity::Member, serenity::UserId, serenity::ReactionType, serenity::GatewayInt # ); ``` +## Unit testing + +Unit testing a Discord bot is difficult, because mocking the Discord API is an uphill battle. +Your best bet for unit testing a Discord bot is to extract the "business logic" into a separate +function - the part of your commands that doesn't call serenity functions - and unit test that. + +Example: + +```rust +# type Error = Box; +# type Context<'a> = poise::Context<'a, (), Error>; +#[poise::command(slash_command)] +pub async fn calc(ctx: Context<'_>, expr: String) -> Result<(), Error> { + let ops: &[(char, fn(f64, f64) -> f64)] = &[ + ('+', |a, b| a + b), ('-', |a, b| a - b), ('*', |a, b| a * b), ('/', |a, b| a / b) + ]; + for &(operator, operator_fn) in ops { + if let Some((a, b)) = expr.split_once(operator) { + let result: f64 = (operator_fn)(a.trim().parse()?, b.trim().parse()?); + ctx.say(format!("Result: {}", result)).await?; + return Ok(()); + } + } + ctx.say("No valid operator found in expression!").await?; + Ok(()) +} +``` + +Can be transformed into + +```rust +# type Error = Box; +# type Context<'a> = poise::Context<'a, (), Error>; +fn calc_inner(expr: &str) -> Option { + let ops: &[(char, fn(f64, f64) -> f64)] = &[ + ('+', |a, b| a + b), ('-', |a, b| a - b), ('*', |a, b| a * b), ('/', |a, b| a / b) + ]; + for &(operator, operator_fn) in ops { + if let Some((a, b)) = expr.split_once(operator) { + let result: f64 = (operator_fn)(a.trim().parse().ok()?, b.trim().parse().ok()?); + return Some(result); + } + } + None +} + +#[poise::command(slash_command)] +pub async fn calc(ctx: Context<'_>, expr: String) -> Result<(), Error> { + match calc_inner(&expr) { + Some(result) => ctx.say(format!("Result: {}", result)).await?, + None => ctx.say("Failed to evaluate expression!").await?, + }; + Ok(()) +} + +// Now we can test the function!!! +#[test] +fn test_calc() { + assert_eq!(calc_inner("4 + 5"), Some(9.0)); + assert_eq!(calc_inner("4 / 5"), Some(0.2)); + assert_eq!(calc_inner("4 ^ 5"), None); +} +``` + # About the weird name I'm bad at names. Google lists "poise" as a synonym to "serenity" which is the Discord library underlying this framework, so that's what I chose.