if you're working in Rust, I recommend that you learn how to work with
macro_rules
. specifically, I recommend this because you can use macros to
build custom assertions that can remove a lot of boilerplate from your test
functions.
for example, if you have an iterator that produces some complicated enum or struct, you could write a macro that will:
- advance the iterator, failing if it doesn't have anything remaining
- match the structure of the contents, failing if it isn't the right variation for the position
- extract value for further assertions
in fact, I can show you that very scenario.
that very scenario
as I've mentioned elsewhere, I'm working on a project called yeet-note. one of the essential components of yeet-note is being able to compare the filesystem tree under a path with an index of that tree.
I wrote a couple of integration tests that cover simple cases of the indexing scan. the indexer produces an iterator containing the results, and in the test scenarios, i expect specific events.
to implement this, I wrote this macro for the test module
macro_rules! expect_next_success { (@next $i:ident, $position:expr) => {{ let next = match $i.next() { None => bail!( "missing an entry @\"{}\"", $position ), Some(v) => v, }; let success = next.context(format_err!( "expected next entry to be a success @\"{}\"", $position ))?; success }}; (@check_var $i: ident, $variant: ident, $position: expr, $msg: expr) => {{ let next_here = expect_next_success!( @next $i, $position ); let res = match next_here { FileIndexEvent::$variant(capture) => capture, o => bail!( "expected event @\"{}\" \ to be {}: {}. instead got {:?}", $position, stringify!($variant), $msg, o ), }; res }}; (created $i: ident, $position:expr, $msg: expr) => { expect_next_success!( @check_var $i, Created, $position, $msg ) }; (updated $i: ident, $position:expr, $msg: expr) => { expect_next_success!( @check_var $i, Updated, $position, $msg ) }; (unchanged $i: ident, $position:expr, $msg: expr) => { expect_next_success!( @check_var $i, Unchanged, $position, $msg ) }; (deleted $i: ident, $position:expr, $msg: expr) => { expect_next_success!( @check_var $i, Deleted, $position, $msg ) }; }
taking advantage of internal rules and the way that tokens are matched, it allows me to specify the events that I expect to see during each scan fairly easily:
let mut res_iter = ops.do_scan(2)?.into_iter(); let res: IndexedFile<i64> = expect_next_success!( created res_iter, "first scan first entry", "first time the entry has been seen" ); assert_eq!(res.entry().name(), "test"); assert_eq!( *res.entry().entry_type(), fs::EntryType::Directory) ;
and follow up assertion with the next one i expect easily.
let res: IndexedFile<i64> = expect_next_success!( created res_iter, "first scan second entry", "first time the entry has been seen" ); assert_eq!(res.entry().name(), "hello.txt"); assert_eq!( *res.entry().entry_type(), fs::EntryType::File ); expect_none_next!(res_iter);
(expect_none_next
is a much simpler macro; see the earlier link)
since I have to do this kind of expectation many times during these tests, having the whole structure factored out makes life easy:
let mut res_iter = ops.do_scan(2)?.into_iter(); let res: IndexedFile<i64> = expect_next_success!( unchanged res_iter, "second scan first entry", "directory had no changes" ); assert_eq!(res.entry().name(), "test"); assert_eq!( *res.entry().entry_type(), fs::EntryType::Directory ); let res: IndexedFile<i64> = expect_next_success!( unchanged res_iter, "second scan second entry", "file had no changes" ); assert_eq!(res.entry().name(), "hello.txt"); assert_eq!( *res.entry().entry_type(), fs::EntryType::File ); expect_none_next!(res_iter);
why not just use a function?
in my specific example, there are several problems you'd run into trying to do it with a function:
- you can't pass the name of a variant into a function and have it generate the appropriate pattern matching.
- knowing a specific iterator type is tricky. however, since the macro expands at compile time, you can simply let the macro generate the iterator calls, and still get the benefit of type checking.
- anything that fails exits directly from the test function itself, making it easier to trace where failures occurred.
learn more
macro_rules
based macros aren't as well documented as other parts of the
language, and there's a lot about them that I found pretty hard to understand.
however, I found this reference guide, The Little Book of Rust Macros, to be helpful in learning both the basics of macro syntax as well as some fairly sophisticated patterns.