Search code examples
unit-testingrusttypeerror

How to test the type of an error in Box<dyn Error>?


I have a function that returns Result<(), Box<dyn Error>>. I am writing a test case for this function where the function should return an error of variant VerifyError::LinearCombinationError (playground):

use std::error::Error;
use std::fmt::{Debug, Display, Formatter};

#[derive(Debug, PartialEq, Eq)]
enum VerifyError {
    LinearCombination,
}

impl Error for VerifyError {}

impl Display for VerifyError {
    fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
        write!(f, "{:?}", self)
    }
}

fn return_result() -> Result<(), Box<dyn Error>> {
    Err(Box::new(VerifyError::LinearCombination))
}

pub fn main() {
    let res = return_result();
    println!("debug print is: {:?}", res);

    // Program does not compile when below line is not commented out
    // assert_eq!(Some(VerifyError::LinearCombination), res.err());
}

Uncommenting the line gives the error message:

error[E0308]: mismatched types
  --> src/main.rs:26:5
   |
26 |     assert_eq!(Some(VerifyError::LinearCombination), res.err());
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected enum `VerifyError`, found struct `Box`
   |
   = note: expected enum `Option<VerifyError>`
              found enum `Option<Box<dyn std::error::Error>>`
   = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)

I am using Rust 1.53.0.


Solution

  • Use Error::is to test if the error trait object has a specific concrete type or Error::downcast to convert it to that concrete type:

    use std::error::Error;
    
    fn example() -> Result<(), Box<dyn Error>> {
        Err(std::io::Error::last_os_error().into())
    }
    
    #[test]
    fn is_from_io_error() {
        let e = example().err().unwrap();
        assert!(e.is::<std::io::Error>());
    
        let _e = e.downcast::<std::io::Error>().unwrap();
    }
    

    If you care about the specific error, I'd encourage you to avoid using trait objects. The point of trait objects is that you don't need to know what the concrete type is.

    Instead, you could use a library that helps you construct error types, such as my SNAFU. This usage shows wrapping errors in an enum that can be pattern matched to test the specific type:

    use snafu::{Snafu, ResultExt}; // snafu = "0.7.0-beta.0"
    
    #[derive(Debug, Snafu)]
    enum Error {
        Example { source: std::io::Error }
    }
    
    fn example() -> Result<(), Error> {
        Err(std::io::Error::last_os_error()).context(ExampleSnafu)
    }
    
    #[test]
    fn is_from_io_error() {
        let e = example();
        assert!(matches!(e, Err(Error::Example { .. })))
    }