Announcing `tested-trait`: associated tests for traits

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 allow where clauses on tests (such as the where Self: Default clause above), but was also considering allowing tests to take an inputs that test_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} where N is an integer, but tested_trait_test_impl_Foo<{T}>_for_Bar<{T}> where T is an arbitrary type expression would be more understandable.

Feedback and ideas appreciated!

2 Likes