Motivated by this reddit comment and this post describing the idea of "unit tests for traits", I spent some time experimenting with a pair of attribute macros for testing trait implementations against tests defined in the trait. I just released them as a crate: tested-trait. Here's an example from the docs:
#[tested_trait]
trait Allocator {
unsafe fn alloc(&mut self, layout: Layout) -> *mut u8;
#[test]
fn alloc_respects_alignment() where Self: Default {
let mut alloc = Self::default();
let layout = Layout::from_size_align(10, 4).unwrap();
for _ in 0..10 {
let ptr = unsafe { alloc.alloc(layout) };
assert_eq!(ptr.align_offset(layout.align()), 0);
}
}
}
#[test_impl]
impl Allocator for alloc::System {
unsafe fn alloc(&mut self, layout: Layout) -> *mut u8 {
alloc::GlobalAlloc::alloc(self, layout)
}
}
The tested_trait
macro allows writing #[test]
tests in a trait definition, and the test_impl
macro instantiates these tests to verify a particular implementation of the trait. More examples and details are in the docs.
When I started, out the idea seemed relatively straightforward, but there were/are a few difficult design questions:
-
How should tests construct instances of the values they're testing? Many traits do not have associated functions or constants producing values of type
Self
, so tests need a separate mechanism for constructing them. I decided to allowwhere
clauses on tests (such as thewhere Self: Default
clause above), but was also considering allowing tests to take an inputs thattest_impl
would somehow provide -- in the end, that seemed less intuitive. -
Should tests be bundled in with the trait definition or defined separately? I decided to require them in the
trait
block for implementation reasons, but it may also be nice to support writing something like:#[tested_trait] trait Foo {} #[trait_test] #[test] fn test_foo<T: Foo>() { .. }
-
Naming the generated tests is tricky: identifiers allow a large set of unicode characters, but notably not a lot of the characters used in types. Currently, their names take the form
tested_trait_test_impl_Foo_{N}
whereN
is an integer, buttested_trait_test_impl_Foo<{T}>_for_Bar<{T}>
whereT
is an arbitrary type expression would be more understandable.
Feedback and ideas appreciated!