Skip to content

Writing your own assertions

This applies both when adding new assertions to this crate or when extending the library in another crate.

Implement the predicate

First write a function that represents the assertion predicate. It will take the inputs as arguments and then return a boolean value. If it returns true, the assertion will pass. If it returns a false, the assertion will fail and cause a panic. It will look something like this:

Unless the inputs implement the Copy trait, make sure to pass them in by reference:

#[doc(hidden)]
pub fn my_assertion_impl(
    input0: &InputType0,
    input1: &InputType1,
    /* ... */
) -> bool {
    /* ... */
}

If the inputs implement the Copy trait, you can pass them in by value:

#[doc(hidden)]
pub fn my_assertion_impl(
    input0: InputType0,
    input1: InputType1,
    /* ... */
) -> bool {
    /* ... */
}

It has to be public so that the macro can access it, but since it is only for internal use we mark it #[doc(hidden)].

Example

Let's reimplement the assert_str_contains!(...) macro from this crate as an example. It will be a bit simplified from the actual implementation:

#[doc(hidden)]
pub fn assert_str_contains_impl(value: &str, substring: &str) -> bool {
    value.contains(substring)
}

Now that we have the perdicate, we can put together the macro itself.

Declare the macro

Once we have the predicate function, we can wrap it in a macro. This will essentially be a wrapper around assert_custom!(...) macro. It will look something like this:

#[macro_export]
macro_rules! my_assertion {
    ($input0:expr, $input1:expr $(, $keys:ident = $values:expr)* $(,)?) => {
        ::test_ur_code_xd::assert_custom!(
            "a description of my assertion's predicate",
            $crate::my_assertion_impl($input0, $input1),
            |panic_message_builder| {
                panic_message_builder
                    .with_argument("input0", stringify!($input0), &$input0)
                    .with_argument("input1", stringify!($input1), &$input1)
            }
            $(, $keys = $values)*
        )
    }
}

This is a lot! Let's break it down.

Macro arguments

The macro will take the inputs as expr arguments. This means that any Rust expression can be used for the assertion inputs. Then we have some additional argument code:

$(, $keys:ident = $values:expr)* $(,)?

This is to accept any number of <key> = <value> arguments which are used to configure the macro. When you pass negate = true into a macro as an additional argument, it goes through this code.

Use assert_custom!(...)

Then we have the call to assert_custom!(...). We pass in all of our custom logic in as the arguments to this macro. This is also the only place where the macro is different between assertions written internally to test ur code XD and outside.

Note

Macros written inside this crate will call this macro like $crate::assert_custom(...) while macros outside will write this ::test_ur_code_xd::assert_custom(...).

Predicate description

The first parameter is a description of the predicate:

"a description of my assertion's predicate"

This is used in the first line of the panic message. For example, the description for assert_str_contains!(...) is "value contains substring". It is important that the inputs are named here so that the panic message's inputs can easily be understood.

Call the predicate function

The second parameter is a call to our predicate function:

Unless the inputs implement the Copy trait, make sure to pass them in by reference:

$crate::my_assertion_impl(&$input0, &$input1)

If the inputs implement the Copy trait, you can pass them in by value:

$crate::my_assertion_impl($input0, $input1)

Build the panic message

The third parameter is a closure which takes a PanicMessageBuilder and returns the same instance. This is used to configure the panic message, usually to add debug information about the inputs:

|panic_message_builder| {
    panic_message_builder
        .with_argument("input0", stringify!($input0), &$input0)
        .with_argument("input1", stringify!($input1), &$input1)
}

Forward keyword arguments

After the three arguments, we need to pass in any <key> = <value> arguments that we want to forward from our macro invocation:

$(, $keys = $values)*

Warning

Make sure not to put a comma before this! It will cause hard to debug compile-time errors. Make sure it's written like this:

// ...
    |panic_message_builder| {
        panic_message_builder
            .with_argument("input0", stringify!($input0), &$input0)
            .with_argument("input1", stringify!($input1), &$input1)
    } // ← no comma here
    $(, $keys = $values)*
// ...

Now your assertion should be functional!

Example

Here's our simplified implementation of the assert_str_contains!(...) macro to use as an example:

#[doc(hidden)]
pub fn assert_str_contains_impl(value: &str, substring: &str) -> bool {
    value.contains(substring)
}

#[macro_export]
macro_rules! assert_str_contains {
    ($value:expr, $substring:expr $(, $keys:ident = $values:expr)* $(,)?) => {
        ::test_ur_code_xd::assert_custom!(
            "value contains substring",
            ::test_ur_code_xd::assertions::string_assertions::assert_str_contains_impl(
                $value,
                $substring
            ),
            |panic_message_builder| {
                panic_message_builder
                    .with_argument("value", stringify!($value), &$value)
                    .with_argument("substring", stringify!($substring), &$substring)
            }
            $(, $keys = $values)*
        )
    };
}

Then you can invoke it like this:

assert_str_contains!("hello, world", "world");
assert_str_contains!("hello, world", "asdf", negate = true);

Writing your own assertion wrapper macros (advanced)

Some assertion macros do not explicitly make an assertion based on a predicate, but instead accept a closure of other assertions. For example assert_outputs!(...) captures output but then relies on closures to make assertions about the captured output:

assert_outputs!(
    || {
        println!("some text");
    },
    on_stdout = |stdout| {
        assert_eq!(stdout, "some text\n");
    }
);

The first step towards writing a macro like this is to, again, write an implementation function. It will look something like this:

#[doc(hidden)]
pub fn my_assertion_wrapper_impl<
    ActionType: FnOnce(),
    ResultCallbackType: FnOnce(ResultType0),
>(
    action: ActionType,
    result_callback: ResultCallbackType,
) {
    // ...

    action();

    // ...

    result_callback(result);
}

Then write a macro to wrap this:

#[macro_export]
macro_rules! my_assertion_wrapper {
    (
        $action:expr,
        on_result = $on_result:expr
    ) => {
        $crate::my_assertion_wrapper_impl(
            $action,
            $on_result,
        )
    };
}

Ironically, this is simpler than implementing a new assertion with a predicate.

Example

For a real world example of this, look at how a simplified version of assert_panics!(...) is implemented. Here is the predicate function:

#[doc(hidden)]
pub fn assert_panics_impl<
    ActionType: FnOnce() + UnwindSafe,
    MessageCallbackType: FnOnce(String),
>(
    action: ActionType,
    on_message: MessageCallbackType,
) {
    // If the action panics:
    if let Err(error) = panic::catch_unwind(AssertUnwindSafe(action)) {
        // Check the message
        on_message(panic_message::panic_message(&error).to_owned());
    } else {
        // Otherwise, fail the assertion
        PanicMessageBuilder::new("action panics", Location::caller()).panic();
    }
}

And then here is a simplified version of the macro:

#[macro_export]
macro_rules! assert_panics {
    ($action:expr, on_message = $on_message:expr) => {
        $crate::assert_panics_impl(
            $action,
            $on_message,
        )
    };
}