Phil Booth

Existing by coincidence, programming deliberately

Custom assertions using Rust macros

Recently I wrote about how Rust macros make it easy to refactor repetitive code that might otherwise become annoying in a strongly-typed language. Continuing the theme from that post, I've noticed another use case where macros can be beneficial: writing custom assertions in tests.

In the unicode-bom crate, there's a bunch of tests that assert the different Unicode byte-order marks are parsed correctly and, just as importantly, that a number of similar byte sequences are not incorrectly identified as byte-order marks.

The cruft in these assertions isn't huge, but it's enough to reduce readability for the key parts of each assertion. Here's what one of them looks like without the assistance of a macro:

assert_eq!(Bom::from(vec![0u8, 0u8, 0xfeu8, 0xffu8].as_slice()), Bom::Utf32Be);

That may not seem too bad at first glance, but in context it swiftly becomes a wall of impenetrable boilerplate:

assert_eq!(Bom::from(vec![0u8, 0u8, 0xfeu8].as_slice()), Bom::Null);
assert_eq!(Bom::from(vec![0u8, 0u8, 0xfeu8, 0xfeu8].as_slice()), Bom::Null);
assert_eq!(Bom::from(vec![0u8, 0u8, 0xfeu8, 0xffu8].as_slice()), Bom::Utf32Be);
assert_eq!(Bom::from(vec![0x0eu8, 0xfeu8].as_slice()), Bom::Null);
assert_eq!(Bom::from(vec![0x0eu8, 0xffu8, 0xfeu8].as_slice()), Bom::Null);
assert_eq!(Bom::from(vec![0x0eu8, 0xfeu8, 0xffu8].as_slice()), Bom::Scsu);

However, a macro to tidy it up is pretty simple:

macro_rules! assert_bom {
    ([$($byte:expr),*], $bom:ident) => {
        assert_eq!(Bom::from(vec![$($byte as u8),*].as_slice()), Bom::$bom)
    }
}

With that in place, the assertions from earlier now look like this:

assert_bom!([0, 0, 0xfe], Null);
assert_bom!([0, 0, 0xfe, 0xfe], Null);
assert_bom!([0, 0, 0xfe, 0xff], Utf32Be);
assert_bom!([0x0e, 0xfe], Null);
assert_bom!([0x0e, 0xff, 0xfe], Null);
assert_bom!([0x0e, 0xfe, 0xff], Scsu);

The benefit here is that a reader's eye is only confronted by the key part of each assertion. All you can see are the byte sequences and the expected result, which means it's easy to zero in on any problems if you're debugging a failing test. And because you're already used to the standard assertion macros like assert! and assert_eq!, a name like assert_bom! is immediately intuitive.