Yuchen
Yuchen

Reputation: 33036

How to test the error message from anyhow::Error?

There is context that can convert an optional value to anyhow::Error which is very convenient.

Simplest way to unwrap an option and return Error if None (Anyhow)

However, how do we test that in unit-tests?

Let's say we have a foo like this:

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

It is easy to test that it is valid output, or that the output is an error. But how do we test the error message from the 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(())
    }
}

Upvotes: 8

Views: 3772

Answers (1)

Locke
Locke

Reputation: 8964

You can use .chain() and .root_cause() to deal with levels of context and use .downcast_ref() or format! to handle the specific error. For example, lets say you had 2 levels of context.

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

In this example the chain would be of the errors "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(())
}

Now, you may have been wondering about my use of format!. It turns out a better solution exists involving downcast_ref, but it requires that your context implement std::error::Error and str does not. Here is an example of this taken directly from the anyhow documentation.

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.
    }
}

As a side note, you may find it easier to use .then() or .then_some() for cases like if input % 2 == 0 { Some(input) } else { None } where you create Some based on a boolean. Simply put, if abc { Some(xyz) } else { None } is equivalent to abc.then(|| xyz). .then_some() passes by value instead of using a closure so I don't usually use it.

Upvotes: 6

Related Questions