How to assert io errors in Rust?

19,016

Solution 1

TL;DR: Error should implement PartialEq

Result<T, E> only implements PartialEq when T and E also implement PartialEq, but io::Error doesn't. alex confirm that cause io::Error takes an extra error that implements dyn Error, allowing the user to add extra information lead std to not implement PartialEq.


I see a lot of answer and comment that seem a lot of peoples use io::Error to create their own error. This is NOT a good practice, io::Error should be used only if you deal yourself with io. There is the Error Handling Project Group if you want to learn and share your view about error in Rust.

For now there is some common crate in Rust to make your own error (feel free to add crate):

I don't totally agree but here a good guide about Error in Rust.


Anyway, the solution that you probably want in your case is just to compare the ErrorKind value. As ErrorKind implements PartialEq this will compile with assert_eq()

use std::io;

fn parse_data(input: i32) -> Result<i32, io::Error> {
    match input {
        0 => Ok(0),
        x => Err(io::Error::new(
            io::ErrorKind::InvalidData,
            format!("unexpected number {}", x),
        )),
    }
}

#[test]
fn test_parsing_wrong_data() {
    let result = parse_data(1).map_err(|e| e.kind());
    let expected = Err(io::ErrorKind::InvalidData);
    assert_eq!(expected, result);
}

Solution 2

I solved it with this

assert!(matches!(result, Err(crate::Error::InvalidType(t)) if t == "foobar"));

This solution doesn't require PartialEq for the Error, but still allows me to compare with the variant contents.

If you don't care about the contents of the variant, then its just

assert!(matches!(result, Err(crate::Error::InvalidType(_))));

Solution 3

If you use the type Result, there is a built in method is_err().

This means you can test with assert!(my_result.is_err());.

Solution 4

In my case I had several structs with differents fields, they all implement the std::error::Error trait. This worked for me in a cleaner way than making an instance of any struct I needed:

let result = parse_data(1);
let your_error = result.unwrap_err().downcast_ref::<MyCustomError>();
assert!(your_error.is_some());

That downcasts the error to a specific type, in case it's the correct type it's a Some(_) value, otherwise, it's None. However, I'm a newbie in Rust, it's probably that this is not the best way to solve your problem.

Solution 5

A macro using Genaritos answer.

macro_rules! is_error_type {
    ($result:ident,$err_type:ident) => {
        $result.unwrap_err().downcast_ref::<$err_type>().is_some()
    }
}

used like this:

let result = parse_data(1);
assert!(is_error_type(result, MyCustomError));
Share:
19,016

Related videos on Youtube

wst
Author by

wst

Kotlin/Java/Go - Software engineering, architecture, design, implementation, testing. Brief technology list: Go, Elasticsearch, NSQ, JavaSE/EE, Kotlin, Spring Boot, Spring Cloud, microservices, distributed systems architecture, devops, docker, container orchestration, Kubernetes, Azure, GoogleCloud, CosmosDB, Cloud Spanner, MySQL, MongoDB, Couchbase, RabbitMq, event sourcing and CQRS - ready to learn more. Mac, Windows and Linux user. Also Ruby/Rails, JSF, Primefaces, HTML, PHP, Javascript, basics of web development. Previous position: Monitoring and first stage diagnosis of major outages and international services and backbone network for GTS CE with wide knowledge of DWDM, SDH, microwave and IP systems, Ethernet L2. Cooperation with international team, close cooperation with colleagues in regards to organize team work, procedures and everyday work. Graphics edition (with a high sense of aesthetic).

Updated on July 08, 2022

Comments

  • wst
    wst almost 2 years

    There are plenty of tutorials showing how to write unit tests in Rust. I've read dozen and all of them focuses on asserting values in case of success. Situation doesn't seem so straight forward in case of an error. Errors do not implement PartialEq trait by default so you can't use assert_eq! macro. Also, some functions may return multiple variants of error depending on what kind of issue occurred (ex. io::Error which may be of different kind.) I could just check if error occurred or not but it doesn't seem enough.

    Example below.

    fn parse_data(input: i32) -> Result<i32, io::Error> {
        match input {
            0 => Ok(0),
            _ => Err(io::Error::new(io::ErrorKind::InvalidData, "unexpected number"))
        }
    }
    
    #[test]
    fn test_parsing_wrong_data() {
        let result = parse_data(1);
        assert!(result.is_err());
        let got = result.unwrap_err();
        let want = io::Error::new(io::ErrorKind::InvalidData, "unexpected number");
    
        // compilation error here: binary operation `==` cannot be applied to type `std::io::Error`
        assert_eq!(want, got);
    }
    

    I assume this is not idiomatic approach, since it's not compiling. Hence the question - what is a proper and idiomatic approach in similar situation?

  • grooveplex
    grooveplex over 4 years
    I have a function returning Result<String, ParseError>, I added #[derive(PartialEq)] to ParseError and it worked!
  • BenSmith
    BenSmith over 4 years
    I found this post very helpful, although I have just a small nit that the expected and result should be swapped in the assertion [i.e. assert_eq!(result, expected);]. I believe this varies between different programming languages!
  • Shepmaster
    Shepmaster over 4 years
    @BenSmith Rust specifically does not ascribe meaning to the first or second argument of assert_eq!. That's why the failure text refers to them as left and right.
  • BenSmith
    BenSmith over 4 years
    Touché! After double checking, it was the IntelliJ IDE returning Expected and Actual in failing tests when run via the “play button”. Indeed one would see left and right when executing the standard (intended) cargo run.
  • Stargateur
    Stargateur over 2 years
    the question already contains this advice assert!(result.is_err()); the question is about how to check the expected error is returned
  • hoijui
    hoijui over 2 years
    This is more powerful then the answers that just check the type, as it also contains the content. Doing that is considered to be an anti-pattern in rust, though, as content (like error strings) can change, and do not allow for compile time verification.
  • scoopr
    scoopr over 2 years
    Well in my case, I actually wanted to test for the content (that the error was from the identifier I expected), which I don't see as anti-pattern at all, I'm not comparing against the Display string. But the pattern match can of course just omit the comparison, if you don't care about the value. I've amended the answer with an example of that.
  • Sam Myers
    Sam Myers over 2 years
    If your error type doesn't have a .kind(), ie. if using thiserror crate, use @scoopr's answer.
  • Stargateur
    Stargateur over 2 years
    @SamMyers not really, the question is about how to assert error in Rust, the general answer is error should implement PartialEq, the .kind() part of my answer is just specific to the OP, sccopr answer is unclear for me.
  • Stargateur
    Stargateur over 2 years
    any reason to not write assert!(result, Err(crate::Error::InvalidType("foobar"))); ? I don't understand your answer please provide a minimal reproducible example
  • Stargateur
    Stargateur over 2 years
    using io::Error, to make its own error is probably a bad idea, look for something like snafu crate
  • Genarito
    Genarito over 2 years
    Hi! Why It would be a bad idea?
  • Stargateur
    Stargateur over 2 years
    io::Error, have never been design to be use that way, it should only be use for io error at max. also downcast on error look... bad. If the error is an opaque type, this mean the error should be used as opaque so no downcast, if your error must be usable for user, then it should not be a opaque type so not a dyn Error but an enum of error possibility. I don't fully agree with lpalmieri.com/posts/error-handling-rust but it's mostly a good guide.
  • Genarito
    Genarito over 2 years
    But I use std::error::Error. Would it have the same problem?
  • Stargateur
    Stargateur over 2 years
    well, as I said, if you wrap all your error into opaque type std::error::Error you should NOT need to do what you do in your answer. Cause you choice to make error opaque. I recommend you to read the guide I link and pick either snafu or thiserror crate. You may be interested by anyhow crate if you want to keep your error opaque.
  • Genarito
    Genarito over 2 years
    Thanks for the reference! I'll check how I could improve my crates!
  • scoopr
    scoopr over 2 years
    If you mean assert_eq, then that requires PartialEq Impl, which my enum didn’t trivially have, and as I pointed out, this solution doesn’t require it. Which is elegant in that it wasn’t needed outside the test anyway.
  • M-Cat
    M-Cat almost 2 years
    Thanks, matches! is just what I needed. Not all errors (e.g. TryFromSliceError) implement PartialEq! I don't know why, but not much I could do about it apart from work around it.
  • M-Cat
    M-Cat almost 2 years
    Also, looks like there is assert_matches in nightly. doc.rust-lang.org/std/assert_matches/macro.assert_matches.ht‌​ml