如何从 Anywhere::Error 测试错误消息?

Yuc*_*ong 5 rust

有一个context可以将可选值转换为anyhow::Error非常方便的。

解开选项并在 None 时返回错误的最简单方法(无论如何)

但是,我们如何在单元测试中测试它?

假设我们有foo这样的:

fn foo(input: i32) -> Result<i32> {
    // this only keep odd numbers
    let filtered = if input % 2 == 0 { Some(input) } else { None };

    filtered.context("Not a valid number")
}
Run Code Online (Sandbox Code Playgroud)

很容易测试它是有效输出,还是输出是错误的。但是我们如何测试来自的错误消息呢context

mod test {
    use super::*;

    #[test]
    fn test_valid_number() -> Result<()> {
        let result = foo(4)?;
        assert_eq!(result, 4);
        Ok(())
    }

    #[test]
    fn test_invalid_number() -> Result<()> {
        let result = foo(3);
        assert!(result.is_err());
        Ok(())
    }

    // error[E0599]: no method named `message` found for struct `anyhow::Error` in the current scope
    //      --> src/main.rs:33:40
    //       |
    //    33 |         assert_eq!(result.unwrap_err().message(), "Not a valid number");
    //       |                                        ^^^^^^^ method not found in `anyhow::Error`
    #[test]
    fn test_invalid_number_error_message() -> Result<()> {
        let result = foo(3);
        assert_eq!(result.unwrap_err().message(), "Not a valid number");
        Ok(())
    }
}
Run Code Online (Sandbox Code Playgroud)

Loc*_*cke 4

您可以使用.chain()and.root_cause()来处理上下文级别,并使用.downcast_ref()orformat!来处理特定的错误。例如,假设您有 2 个级别的上下文。

use anyhow::*;

fn bar(input: i32) -> Result<i32> {
    // this only keep odd numbers
    let filtered = if input % 2 == 0 { Some(input) } else { None };

    filtered.context("Not a valid number")
}


fn foo(input: i32) -> Result<i32> {
    return bar(input).context("Handled by bar")
}
Run Code Online (Sandbox Code Playgroud)

在此示例中,链将是错误"Handled by bar"-> "Not a valid number"

#[test]
fn check_top_error() -> Result<()> {
    let result = foo(3);
    let error = result.unwrap_err();
        
    // Check top error or context
    assert_eq!(format!("{}", error), "Handled by bar");
    
    // Go down the error chain and inspect each error
    let mut chain = error.chain();
    assert_eq!(chain.next().map(|x| format!("{x}")), Some("Handled by bar".to_owned()));
    assert_eq!(chain.next().map(|x| format!("{x}")), Some("Not a valid number".to_owned()));
    assert_eq!(chain.next().map(|x| format!("{x}")), None);
    
    Ok(())
}

#[test]
fn check_root_cause() -> Result<()> {
    let result = foo(3);
    let error = result.unwrap_err();
    
    // Equivalent to .chain().next_back().unwrap()
    let root_cause = error.root_cause();
    
    assert_eq!(format!("{}", root_cause), "Not a valid number");
    Ok(())
}
Run Code Online (Sandbox Code Playgroud)

现在,您可能想知道我对format!. 事实证明,存在一个更好的解决方案,涉及downcast_ref,但它要求您的上下文实现std::error::Error,但str没有。anyhow下面是直接取自文档的示例。

use anyhow::{Context, Result};

fn do_it() -> Result<()> {
    helper().context(HelperFailed)?;
    ...
}

fn main() {
    let err = do_it().unwrap_err();
    if let Some(e) = err.downcast_ref::<HelperFailed>() {
        // If helper failed, this downcast will succeed because
        // HelperFailed is the context that has been attached to
        // that error.
    }
}
Run Code Online (Sandbox Code Playgroud)

作为旁注,您可能会发现它更易于使用.then(),或者.then_some()适用于基于布尔值if input % 2 == 0 { Some(input) } else { None }创建的情况。Some简单来说,if abc { Some(xyz) } else { None }相当于abc.then(|| xyz).then_some()按值传递而不是使用闭包,因此我通常不使用它。