NUnit is a C# unit-test framework that permits you to write code like this:
Assert.That(someInt, Is.EqualTo(42));
Assert.That(someList, Has.Member(someMember));
I like this kind of code because it is easily readable by looking like English.
I am playing with Rust to see if I can create a library that gives the same feelings:
use std::fmt::Debug;
struct Is;
enum Verb<T> {
EqualTo(T),
}
impl Is {
fn equal_to<T>(&self, obj: T) -> Verb<T> {
Verb::EqualTo(obj)
}
}
#[allow(non_upper_case_globals)]
const is: Is = Is{};
fn assert_that<T: Eq + Debug>(obj: T, verb: Verb<T>) {
match verb {
Verb::EqualTo(rhs) => assert_eq!(obj, rhs),
}
}
fn main() {
assert_that(42, is.equal_to(42));
assert_that(42, is.equal_to(0));
}
This is good, but for one thing: when the code panics at assert_that(42, is.equal_to(0))
, the line given by the panic is the line of assert_eq!(obj, rhs)
(i.e. in the library instead of user's code). I know this behavior is normal, but I would have a more useful message.
How to indicate the right line number in the panic?
There's no direct way to adjust the line number that panic!
prints.
There is a proto-RFC to add an attribute that would allow certain methods to be "hidden" from backtraces. It's possible that such an attribute would also affect the line number, but it's unclear.
How to write a panic! like macro in Rust? describes how you could write your own panic!
macro, but it chooses to tear down the entire process, not just the current thread.
The important thing is that you just want to control the message, which is possible via panic::set_hook
. You can pass side-channel information from the test to the panic handler via thread locals.
use std::cell::Cell;
thread_local! {
static ASSERT_LOCATION: Cell<Option<(&'static str, u32)>> = Cell::new(None)
}
fn report_my_error(info: &std::panic::PanicInfo) {
match info.location() {
Some(location) => {
let file = location.file();
let line = location.line();
println!("The panic actually happened at: {}, {}", file, line);
}
None => println!("I don't know where the panic actually happened"),
}
ASSERT_LOCATION.with(|location| if let Some((file, line)) = location.get() {
println!(
"But I'm going to tell you it happened at {}, {}",
file,
line
);
});
if let Some(msg) = info.payload().downcast_ref::<&str>() {
println!("The error message was: {}", msg);
}
}
#[test]
fn alpha() {
std::panic::set_hook(Box::new(report_my_error));
ASSERT_LOCATION.with(|location| {
location.set(Some((file!(), line!())));
});
panic!("This was only a test")
}
You need to ensure your panic handler is set in each test and then set the location information. You may also want to update the panic handler to set the location information back to None
to avoid location information leaking between threads.
You will likely want to write your own macro that the user can use in the test to set the line number implicitly. Syntax similar to this could give a place for this setup code to live:
assert_that!(42, is.equal_to(0));
Could expand to:
assert_that(file!(), line!(), 42, is.equal_to(0));
And I'd probably set that panic handler inside of assert_that
.
You may interested in using the spectral, a library that provides fluent test assertions for Rust. If you look at their implementation, as others suggest, they use a macro instead of a function, so line!()
and file!()
macros are expanded in place where you put the assert_that!
macro defined in the library.
Usage is as follows:
#[test]
fn example() {
assert_that!(2).is_equal_to(4);
}
And output, as expected, points to the right line in my library:
failures:
---- utils::tests::example stdout ----
thread 'utils::tests::example' panicked at '
expected: <4>
but was: <2>
at location: src/utils.rs:344
', /home/example/.cargo/registry/src/github.com-1ecc6299db9ec823/spectral-0.6.0/src/lib.rs:343
note: Run with `RUST_BACKTRACE=1` for a backtrace.
failures:
utils::tests::example
test result: FAILED. 61 passed; 1 failed; 0 ignored; 0 measured