Unit-tests for traits?

Problem

I would like to write a test-suite for a trait. So that all implementations of the trait could instantiate the entire test-suit.

Google Test for C++ has a more detailed description of the goal:

Suppose you have multiple implementations of the same interface and want to make sure that all of them satisfy some common requirements.

If you are designing an interface or concept, you can define a suite of type-parameterized tests to verify properties that any valid implementation of the interface/concept should have. Then, the author of each implementation can just instantiate the test suite with their type to verify that it conforms to the requirements, without having to write similar tests repeatedly. Here’s an example ...

Ad-hoc solution in Rust could be:

File with trait:

pub trait Sensor {
   fn new(id: i32, path: &str) -> Self;
   fn id(&self) -> i32;
   ...
}

mod tests {
   // A test for the id() getter that any implementation should satisfy:
   pub fn id_can_be_fetched<T: Sensor>() {
      let sensor = T::new(1234, "asdf");
      assert_eq!(sensor.id(), 1234);
   }
}

File with implementation:

impl crate::Sensor for Sensor {
   fn new(id: i32, path: &str) -> Self {
      Sensor { path: path.to_string(), id }
   }
   fn id(&self) -> i32 { self.id }
}

mod tests {
   // "Instantiate" trait test:
   #[test]
   fn id_can_be_fetched() {
      crate::tests::id_can_be_fetched::<Sensor>();
   }
}

But it is not very elegant, there is quite a bit of repetition, etc...

Question

What is the proper / idiomatic way in Rust to write tests for traits?

Rust's test harness doesn't have anything like this built in, and we don't have any fancy reflection mechanisms which would let you automatically discover trait implementations or dynamically create tests on the fly. You'd probably have to write your own abstractions for a "generic test suite" that can be instantiated with different types, possibly using a macro to generate individual tests like id_can_be_fetched() (I could see some sort of custom attribute for traits coming in handy here).

How would you implement this in C++? Maybe you can take some inspiration from that.

How would you implement this in C++? Maybe you can take some inspiration from that

In C++, from the point of view of end-user, it boils down to these things:

  • In the file where your trait is, you define a set of test functions and then register them via macros:

      REGISTER_TYPED_TEST_SUITE_P(SensorTraitTestSuit,
                               id_can_be_fetched,
                               path_can_be_fetched,
                               <other functions>)
    

    I guess at this point there is some sort of static array of some objects/wrappers that know what function to call.

  • And then, when you want instantiate the test-suite, you use another macros where you mention trait test suite and your implementation:

    INSTANTIATE_TYPED_TEST_SUITE_P(SomeName, SensorTraitTestSuit, MyImplementationOfTrait);
    

    I guess at this point, some sort of type-erasure happens, and all the provided types/objects get wrapped into callable objects/lambdas, which can then be stored in another static array and called at run-time.

GTest just uses macros to declare a list of functions and implementations, so one could probably write a simple Rust macro to do similar:

macro_rules! sensor_test {
    ($t:ty) => {
        sensor_test!($t, id_can_be_fetched);
        sensor_test!($t, another_test);
        // ... more tests go here ...
    };
    ($t:ty, $name:ident) => {
        #[test]
        fn $name() {
            $crate::tests::$name::<$t>();
        }
    };
}
impl crate::Sensor for Sensor {
   // ...
}

#[cfg(tests)]
mod tests {
      sensor_test!(super::Sensor);
}

I was meaning to write a set of procedural attribute macros, testify, for doing this, but I haven't gotten around to it yet.

1 Like