unit-testingrusttraitstrait-objects

How to unit test two implementations of a trait?


I have two structs implementing the same trait using a different algorithm. I want to write the unit tests once and run them against both structs. What is the best way to do this?

My concern is to test both implementations as black box and avoid code duplication as much as possible.

[rust newbie] Here is a non compilable source code to show what I tried. I am not interested in dynamic dispatching - The production code will use just one of the implementations.

pub trait Searchable<T> {
    fn search(&self, item: &T, radius: f32) -> Vec<T>;
}

struct Item { x: u32, y: u32}

struct A { some_field: u32 }

impl Searchable<Item> for A {
    fn search(&self, _item: &Item, _radius: f32) -> Vec<Item> { vec![] }
}

struct B {}

impl Searchable<Item> for B {
    fn search(&self, _item: &Item, _radius: f32) -> Vec<Item> { vec![] }
}

#[cfg(test)]
mod tests {
    use super::*;

    /// Create a new Searchable and a vector of Items.
    /// The Searchable will be either A or B, depending on the value of use_a.
    fn setup<T>(use_a: bool) -> (dyn Searchable<T>, Vec<Item>) {
                            //  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ doesn't have a size known at compile-time

        // generate some data
        (if use_a {A{}} else {B{}}, Vec::new())
    }

    #[test]
    fn which_search() {
        let (mut store, items) = setup(true);
        // do some searches and assertions
    }
}

fn main() {} // to sooth the compiler

Tried changing the code to be able to do dynamic dispatching (just for the test), but did not succeed.


Solution

  • Rust doesn't have a good framework for "parameterized" tests, that is tests that take apply to multiple values.

    In such a case, the easiest way is to create a function which takes the unit-under-test as a parameter, and additionally the test case inputs (if any), then call that function with the various values/inputs that you desire.

    There are two styles:

    Note that if the function is a one-off -- only used in a single test -- it is best declared within that test itself, so as to make that clear.

    Example of simple test:

    #[test]
    fn no_neighbour() {
        fn do_search<T: Searchable<Item>>(mut searchable: T) -> Vec<Item> {
            searchable.search(&Item::default(), 0.0)
        }
    
        assert_eq!(Vec::new(), do_search(A { some_field: 42 }));
        assert_eq!(Vec::new(), do_search(B{}));
    }
    

    Example of more complex test:

    #[test]
    fn four_neighbours() {
        #[track_caller]
        fn assert_search<T: Searchable<Item>>(mut searchable: T, expected: &[Item]) {
            let actual = searchable.search(&Item::default(), 0.0);
    
            assert_eq!(expected, actual);
        }
    
        assert_search(A { some_field: 42 }, &[...]);
        assert_search(B{}, &[...]);
    }
    

    There are other possibilities. For example, if both implementations should return identical results in all circumstances -- and just employ a different algorithm -- you could implement a cross-checking function which takes both + a serie of inputs, and apply each input in turn, asserting the same result is produced.


    As to which your code is not working: you cannot just return a "bare" dyn X because that's a Dynamically Sized Type. Remember that in Rust, unless you explicitly tell otherwise, a value is passed on the stack, and the amount of memory it'll take on the stack must be known at compile-time... which is not possible for Dynamically Sized Types.

    Typically, Dynamically Sized Types are passed by references, or using pointers, as appropriate. In your case, you'd need to return a Box<dyn Searchable<Item>> instead.

    Do be careful about lining up your ducks correctly: Searchable<T> takes a T and returns a Vec<T>... so you can't have a Searchable<T> (unspecified T) take an Item (specific concrete type), your setup function must return a Searchable<Item> if you wish to use the result on Item(s).