0

Context & my attempts

I am trying to unit-test a function, and I want to make sure it calls the correct API endpoint with the correct arguments.

My first idea was to make the function I wanted to test accept the API as a trait object. This would allow me to mock it. I also had to pass mutable references, because the mock should record information about how it was called. Below you see a simplified representation of my code structure:

trait API {
    fn func_a(&mut self, arg1: i32, arg2: String);
}

// prod trait implementation

#[allow(dead_code)]
fn business_logic(api: &mut dyn API) {
    api.func_a(42, String::from("mytrousersareblue"));
}

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

    #[derive(Default)]
    struct Mock<A, B> {
        args: (A, B),
    }
    impl API for Mock<i32, String> {
        fn func_a(&mut self, arg1: i32, arg2: String) {
            self.args = (arg1, arg2);
        }
    }

    #[test]
    fn business_logic_performs_correct_calls() {
        let mut mock: Mock<i32, String> = Default::default();
        business_logic(&mut mock);
        assert_eq!(mock.args.0, 42);
        assert_eq!(mock.args.1, String::from("mytrousersareblue"));
    }
}

This has various downsides, mainly that I have to make separate Mock structs for functions with zero, one, two, three, and more parameters.

So another idea that came to my mind was to implement the Mock struct as follows:

struct Mock<'a> {
    args: Vec<&'a dyn Eq>,
}

This would allow me to record as many arguments as I want. However the compiler complains that the trait std::cmp::Eq cannot be made into an object.

I cannot use std::any::Any, because I still need to be able to determine equality in the assert_eq statements.

Maybe I am taking a completely wrong path here. I do have quite some trouble with the Rust mindset, coming from a Java background...

Summary / TLDR

I want to have a mock-implementation of the API, which records all arguments it was called with, that can later be compared against expected values.

If I'm correct, then I'll need a way to make a struct store any number of objects, which all extend at least Eq. However, if there's a nicer solution: I'm open.

TobTobXX
  • 418
  • 4
  • 17
  • 1
    You should probably take a look at the [mockall](https://crates.io/crates/mockall/) crate. This uses macros to derive mocks for a particular trait. – Richard Matheson May 21 '20 at 21:35
  • 1
    Yup, thanks! That turned out to be the solution for my case. However, I found an article showing how to tackle this problem without packages and without macros. As soon as I understand it enough to explain it, I'll post an answer to it. The blog post is the following: https://dev.to/magnusstrale/rust-trait-objects-in-a-vector-non-trivial-4co5 – TobTobXX May 22 '20 at 15:46

1 Answers1

0

I've found two solutions:

The practical one:

Use the mockall crate (thanks @Richard Matheson for the recommendation). Seriously, don't try to re-implement mocking yourself. The mockall crates does it using macros, and it is pure magic. But it works.

The nightmare:

I don't recommend using this answer, but it is helpful to know as a design concept.

The way to solve this problem, is to declare a trait implementing Any and defining the following functions:

trait Arg: Any {
    fn as_any(&self) -> &dyn Any;
    fn equals_arg(&self, _: &dyn Any) -> bool;
}

Then you implement this trait for as many structs as possible:

impl<S: 'static + PartialEq> Arg for S {
    fn as_any(&self) -> &dyn Any {
        self
    }

    fn equals_arg(&self, other: &dyn Arg) -> bool {
        other
            .as_any()
            .downcast_ref::<S>()
            .map_or(false, |a| self == a)
    }
}

Implementing the as_any function is straightforward. The equals_arg function is trickier. We compare these two references by first trying to downcast the other object to the type of self. If that fails, we immediately return false. If it succeeds, we compare them using the == operator, made available because both implement the PartialEq trait.

We can now store an array of dyn Arg elements and compare them using equals_arg. See the playground for an example implementation. (In the playground I changed the API interface to accept &'static types. This could certainly be improved, but I am a beginner concerning lifetimes.)

References

mcarton
  • 27,633
  • 5
  • 85
  • 95
TobTobXX
  • 418
  • 4
  • 17