如何在Rust中断言IO错误?

49

有很多教程展示如何在Rust中编写单元测试。我已经阅读了数十篇,它们都专注于在成功的情况下断言值。但是,在出现error的情况下,情况似乎并不那么简单。错误默认情况下不实现PartialEq trait,因此您无法使用assert_eq!宏。此外,某些函数可能会返回多个错误变体,这取决于发生了什么样的问题(例如io::Error可以是不同类型的错误)。我只能检查是否发生了错误,但这似乎不够。

以下是示例。

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

我认为这不是惯用的方法,因为它没有编译。因此问题是 - 在类似情况下什么是适当和惯用的方法?

5个回答

33
TL;DR: Error 应该实现 PartialEq
TE 也实现 PartialEq 时,Result<T, E> 才会实现 PartialEq,但是 io::Error 没有实现。alex 确认这是因为 io::Error 接受一个额外的实现了 dyn Error 的错误,允许用户添加额外信息,导致 std 不实现 PartialEq
我看到许多答案和评论,似乎许多人使用 io::Error 来创建自己的错误。这是不好的做法,io::Error 应该仅在处理 io 时使用。如果您想学习和分享有关Rust中的错误的观点,则有 错误处理项目组

目前在Rust中有一些常见的箱子可用于制作自己的错误(请随意添加箱子):

我不完全同意,但这里有一个很好的指南,介绍了Rust中的错误处理。


无论如何,在您的情况下,您可能想要的解决方案只是比较ErrorKind的值。由于ErrorKind实现了PartialEq,因此这将与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);
}

我有一个返回 Result<String, ParseError> 的函数,我给 ParseError 添加了 #[derive(PartialEq)],然后它就可以工作了! - grooveplex
我发现这篇文章非常有帮助,尽管我只有一个小问题,就是在断言中期望值和结果应该交换 [即 assert_eq!(result, expected);]。我相信这在不同的编程语言中可能会有所不同! - BenSmith
3
@BenSmith Rust特别指出assert_eq!的第一个或第二个参数没有任何意义。这就是为什么失败文本将它们称为“left”和“right”的原因。 - Shepmaster
太好了!经过仔细检查,发现是 IntelliJ IDE 在通过“播放按钮”运行失败的测试时返回了预期和实际结果。确实,在执行标准(预期的)cargo run 时会看到左右两个结果。 - BenSmith
如果您的错误类型没有 .kind(),即如果使用 thiserror crate,请使用 @scoopr 的答案。 - Sam Myers
@SamMyers,不完全是这样的,这个问题是关于如何在Rust中断言错误的,一般的答案是错误应该实现PartialEq,我的回答中的.kind()部分只是针对OP具体情况的,sccopr的回答对我来说不够清楚。 - Stargateur

30
我用以下方法解决了这个问题。
assert!(matches!(result, Err(crate::Error::InvalidType(t)) if t == "foobar"));


这个方案不需要 PartialEq 用于 Error,但仍允许我与变量内容进行比较。
如果您不关心变量的内容,则只需
assert!(matches!(result, Err(crate::Error::InvalidType(_))));


这比仅检查类型的答案更强大,因为它还包含内容。然而,在 Rust 中这样做被认为是反模式,因为内容(如错误字符串)可能会发生变化,不允许在编译时进行验证。 - hoijui
3
对于我的情况,我实际上想测试内容(即错误来自我预期的标识符),我并不认为这是反模式,我没有与“Display”字符串进行比较。但是,如果你不关心值,模式匹配当然可以省略比较。我已经修改了答案,并给出了一个例子。 - scoopr
有没有任何理由不写 assert!(result, Err(crate::Error::InvalidType("foobar")));?我不明白你的回答,请提供一个 [mcve]。 - Stargateur
如果你的意思是 assert_eq,那么它需要 PartialEq 实现,而我的枚举并没有显然具备这个特性。正如我所指出的,这个解决方案并不需要它。这种方法很优雅,因为在测试之外根本不需要它。 - scoopr
谢谢,“匹配!matches”正是我所需要的。并非所有错误(例如“TryFromSliceError”)都实现了“PartialEq”!我不知道为什么,但除了解决方法外,我别无选择。 - Marcin
1
另外,夜间版似乎有assert_matches。https://doc.rust-lang.org/std/assert_matches/macro.assert_matches.html - Marcin

7
如果您使用类型为Result,则有一个内置的方法is_err()
这意味着您可以使用assert!(my_result.is_err());进行测试。

2
问题已经包含了这个建议 assert!(result.is_err());,问题是如何检查是否返回了预期的错误。 - Stargateur

4
在我的情况下,我有几个具有不同字段的结构体,它们都实现了std::error::Error特性。对我来说,这比创建任何我需要的结构体实例更加简洁。
let result = parse_data(1);
let your_error = result.unwrap_err().downcast_ref::<MyCustomError>();
assert!(your_error.is_some());

将错误降级(downcasts)为特定类型,如果是正确的类型,则返回 Some(_) 值,否则返回 None。然而,我在 Rust 中还是新手,可能这不是解决问题的最佳方式。


使用io :: Error来制作自己的错误可能不是一个好主意,可以尝试寻找类似snafu crate的东西。 - Stargateur
嗨!为什么这是个坏主意? - Genarito
io::Error从未设计成那样使用,它应该仅用于最大程度的I/O错误。此外,错误下转型看起来...不好。如果错误是不透明类型,则意味着应将错误用作不透明,因此不会下转型;如果您的错误必须对用户可用,则它不应是不透明类型,而是错误可能性的枚举类型(而非动态错误)。我并不完全同意https://www.lpalmieri.com/posts/error-handling-rust/,但这是一个很好的指南。 - Stargateur
但是我使用 std::error::Error。它会有同样的问题吗? - Genarito
好的,就像我说的那样,如果你将所有的错误都封装成不透明类型 std::error::Error,你就不需要在你的答案中做你所做的事情。因为你选择了让错误不透明。我建议你阅读我提供的指南,并选择 snafu 或 thiserror crate 中的一个。如果你想保持错误不透明,你可能会对 anyhow crate 感兴趣。 - Stargateur
谢谢您的参考!我会查看如何改进我的板条箱! - Genarito

1
一个使用 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));

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接