How do I assert an enum is a specific variant if I don't care about its fields?
Asked Answered
G

3

61

I'd like to check enums with fields in tests while ignoring the actual value of the fields for now.

Consider the following example:

enum MyEnum {
    WithoutFields,
    WithFields { field: String },
}

fn return_with_fields() -> MyEnum {
    MyEnum::WithFields {
        field: "some string".into(),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn example() {
        assert_eq!(return_with_fields(), MyEnum::WithFields {..});
    }
}

playground

I'd like to use assert_eq! here, but the compiler tells me:

error: expected expression, found `}`
  --> src/lib.rs:18:64
   |
18 |         assert_eq!(return_with_fields(), MyEnum::WithFields {..});
   |                                                                ^ expected expression

This is similar to Why do I get an error when pattern matching a struct-like enum variant with fields?, but the solution does not apply in my case.

Of course, I can use match and do it myself, but being able to use assert_eq! would be less work.

Gemology answered 1/7, 2018 at 7:56 Comment(6)
What MyEnum::WithFields {..} is suppose to do ? play.rust-lang.org/…Leelah
Hi :) I think your question is answered by "Compare enums only by variant, not value". In short: use mem::discriminant. If you don't think this link answers your question, please explain why your question is different.Festal
@Leelah it is supposed to ignore the fields value and just match on WithFields.Gemology
@LukasKalbertodt Sadly that does not work, the rust compiler still wants me to specify the field somehow.Gemology
@PhilippLudwig You can't compare a variant with something that doesn't exist, do you want something like that ? play.rust-lang.org/…Leelah
@Leelah yeah, I'd like something like that. Seems a bit much though.Gemology
O
76

Rust 1.42

You can use std::matches:

assert!(matches!(return_with_fields(), MyEnum::WithFields { .. }));

Previous versions

Your original code can be made to work with a new macro:

macro_rules! is_enum_variant {
    ($v:expr, $p:pat) => (
        if let $p = $v { true } else { false }
    );
}

#[test]
fn example() {
    assert!(is_enum_variant!(return_with_fields(), MyEnum::WithoutFields {..}));
}

Personally, I tend to add methods to my enums:

fn is_with_fields(&self) -> bool {
    match self {
        MyEnum::WithFields { .. } => true,
        _ => false,
    }
}

I also tend to avoid struct-like enums and instead put in extra work:

enum MyEnum {
    WithoutFields,
    WithFields(WithFields),
}

struct WithFields { field: String }

impl MyEnum {
    fn is_with_fields(&self) -> bool {
        match self {
            MyEnum::WithFields(_) => true,
            _ => false,
        }
    }

    fn as_with_fields(&self) -> Option<&WithFields> {
        match self {
            MyEnum::WithFields(x) => Some(x),
            _ => None,
        }
    }
    
    fn into_with_fields(self) -> Option<WithFields> {
        match self {
            MyEnum::WithFields(x) => Some(x),
            _ => None,
        }
    }
}

I hope that some day, enum variants can be made into their own type to avoid this extra struct.

Ogden answered 1/7, 2018 at 13:51 Comment(2)
A gotcha I ran into with assert!(matches!(...)) is that, unlike assert_eq!, the argument order matters, and you must use a literal for the second argument. assert!(matches!(exp_result, act_result)) will never panic, because exp_result can always be matched with the "name" act_result. You need to destructure at least one level in order to trigger an actual match, like the presented assert!(matches!(exp_result, MyEnum::WithFields { .. })). Somewhat obvious in hindsight, but maybe this comment will help someone save a few minutes of debugging.Nicotine
@SeanKelleher You'd also get a warning from the compiler: warning: unused variable: `act_result`Ogden
S
7

If you are using Rust 1.42 and later, see Shepmaster's answer below.

A simple solution here would be to do the opposite assertion:

assert!(return_with_fields() != MyEnum::WithoutFields);

or even more simply:

assert_ne!(return_with_fields(), MyEnum::WithoutFields);

Of course if you have more members in your enum, you'll have to add more asserts to cover all possible cases.

Alternatively, and this what OP probably had in mind, since assert! just panics in case of failure, the test can use pattern matching and call panic! directly in case something is wrong:

match return_with_fields() {
    MyEnum::WithFields {..} => {},
    MyEnum::WithoutFields => panic!("expected WithFields, got WithoutFields"),
}
Superfetation answered 1/7, 2018 at 10:10 Comment(2)
Thanks, that is a good idea. I'm still wondering though if there is a more straight-forward solution. Or maybe I will just have to create my own macro.Gemology
@Superfetation Thanks for providing the source, this is interesting. I will accept your answer, maybe you want to include these additional information there.Gemology
K
2

I'd use a macro like @Shepmaster proposed, but with more error reporting (like the existing assert! and assert_eq! macros:

macro_rules! assert_variant {
    ($value:expr, $pattern:pat) => ({
        let value = &$value;

        if let $pattern = value {} else {
            panic!(r#"assertion failed (value doesn't match pattern):
   value: `{:?}`,
 pattern: `{}`"#, value, stringify!($pattern))
        }
    })

    // TODO: Additional patterns for trailing args, like assert and assert_eq
}

Rust playground demonstrating this example

Kenway answered 27/9, 2019 at 1:22 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.