[][src]Macro double::mock_method

macro_rules! mock_method {
    ( $method:ident(&self $(,$arg_name:ident: $arg_type:ty)*)) => { ... };
    ( $method:ident(&self $(,$arg_name:ident: $arg_type:ty)*), $sel:ident, $body:tt ) => { ... };
    ( $method:ident<($($type_params: tt)*)>(&self $(,$arg_name:ident: $arg_type:ty)*),
        $sel:ident, $body:tt) => { ... };
    ( $method:ident(&self $(,$arg_name:ident: $arg_type:ty)*) -> $retval:ty ) => { ... };
    ( $method:ident(&self $(,$arg_name:ident: $arg_type:ty)*) -> $retval:ty, $sel:ident, $body:tt ) => { ... };
    ( $method:ident<($($type_params: tt)*)>(&self $(,$arg_name:ident: $arg_type:ty)*)
        -> $retval:ty, $sel:ident, $body:tt ) => { ... };
    ( $method:ident(&mut self $(,$arg_name:ident: $arg_type:ty)*)) => { ... };
    ( $method:ident(&mut self $(,$arg_name:ident: $arg_type:ty)*), $sel:ident, $body:tt ) => { ... };
    ( $method:ident<($($type_params: tt)*)>(&mut self $(,$arg_name:ident: $arg_type:ty)*),
        $sel:ident, $body:tt) => { ... };
    ( $method:ident(&mut self $(,$arg_name:ident: $arg_type:ty)*) -> $retval:ty ) => { ... };
    ( $method:ident(&mut self $(,$arg_name:ident: $arg_type:ty)*) -> $retval:ty, $sel:ident, $body:tt ) => { ... };
    ( $method:ident<($($type_params: tt)*)>(&mut self $(,$arg_name:ident: $arg_type:ty)*)
        -> $retval:ty, $sel:ident, $body:tt ) => { ... };
}

Macro that generates a mock implementation of a trait method.

This should be used to implement a trait on a mock type generated by double's mock_trait macro. If one has generated a mock struct using mock_trait, then the actual implementation of the desired trait can be auto-generated using mock_method, like so:


trait TaskManager {
   fn max_threads(&self) -> u32;
   fn set_max_threads(&mut self, max_threads: u32);
}

mock_trait!(
    MockTaskManager,
    max_threads(()) -> u32,
    set_max_threads(u32) -> ()
);

// Actually implement the trait that should be mocked
impl TaskManager for MockTaskManager {
    mock_method!(max_threads(&self) -> u32);
    mock_method!(set_max_threads(&mut self, max_threads: u32));
}

let mut mock = MockTaskManager::default();
mock.max_threads.return_value(42u32);
assert_eq!(42, mock.max_threads());
assert!(mock.max_threads.called_with(()));
mock.set_max_threads(9001u32);
assert!(mock.set_max_threads.called_with(9001u32));

There are many different variants of mock_method. In total there are 12 variants. 8 variants provides a combination of the following:

  1. const method (&self) or mutable method (&mut self)
  2. return value (fn foo(&self) -> bool) or no return value (fn foo(&self))
  3. automatically generated method body or custom method body

(1) allows both constant and mutable methods tobe mocked, like in the MockTaskManager example above.

(2) is for convenience. It means one doesn't have to specify -> () explicitly for mocked methods that don't return values. This can also be shown in the MockTaskManager example. Notice how the return type is not specified when generating the set_max_threads() mock.

(3) is useful when the stored call arguments' types (defined by the mock_trait() macro) are different to the mocked method. There are cases where type differences in the stored args and the actual method args are required. For example, suppose you had the following trait:

trait TextStreamWriter {
    fn write(text: &str);
}

A mock can't store received text arguments as &str because the mock needs to the own the given arguments (and &str is a non-owning reference). Therefore, the mock trait has to be specified like so:


trait TextStreamWriter {
    fn write(&mut self, text: &str);
}

mock_trait!(
    MockTextStreamWriter,
    // have to use `String`, not `&str` here, since `&str` is a reference
    write(String) -> ()
);

impl TextStreamWriter for MockTextStreamWriter {
    mock_method!(write(&mut self, text: &str), self, {
        // manually convert the reference to an owned `String` before
        // passing it to the underlying mock object
        self.write.call(text.to_owned())
    });
}
    // only here to make `cargo test` happy
}

Using variant (3) of mock_method means we specify the body of the generated function manually. The custom body simply converts the &str argument to an owned string and passes it into the underlying write Mock object manually. (normally auto-generated bodies do this for you).

The name of the underlying mock object is always the same as the mocked method's name.

&str parameters are common. It can be inconvenient haven't to manually specify the body each time they appear. There are plans to add a macro to generate a body that calls to_owned() automatically. (TODO: implement the macro)

Type Parameters

There are an additional 4 variants to handle method type parameters (e.g. fn foo<T: Eq>(&self, a: &T)). These variants allow one to generate mock methods which take some generic type parameters.

For example, suppose one had a Comparator trait that was responsible for comparing any two values in the program. It might look something like this:

trait Comparator {
   fn is_equal<T: Eq>(&self, a: &T, b: &T) -> bool;
}

T can be multiple types. Currently, we cannot store call arguments that have generic types in the underlying Mock objects. Therefore, one has to convert the generic types to a different, common representation. One way to get around this limitation is converting each generic type to a String. e.g. for the Comparator trait:


use std::string::ToString;

trait Comparator {
   fn is_equal<T: Eq + ToString>(&self, a: &T, b: &T) -> bool;
}

mock_trait!(
    MockComparator,
    // store all passed in call args as strings
    is_equal((String, String)) -> bool
);

impl Comparator for MockComparator {
    mock_method!(is_equal<(T: Eq + ToString)>(&self, a: &T, b: &T) -> bool, self, {
        // Convert both arguments to strings and manually pass to underlying
        // mock object.
        // Notice how the both arguments as passed as a single tuple. The
        // underlying mock object always expects a single tuple.
        self.is_equal.call((a.to_string(), b.to_string()))
    });
}
    // only here to make `cargo test` happy
}

If the to_string conversions for all T are not lossy, then our mock expectations can be exact. If the to_string conversions are lossy, then this mechanism can still be used, providing all the properties of the passed in objects are captured in the resultant Strings.

This approach requires the writer to ensure the code under test adds the ToString trait to the trait's type argument constraints. This limitation forces test writers to modify production code to use double for mocking.

Despite this, there is still value in using double for mocking generic methods with type arguments. Despite adding boilerplate to production code and manually implementing mock method bodies being cumbersome, the value add is that all argument matching, expectations, calling test functions, etc. are all still handled by double. Arguably, reimplenting those features is more cumbersome than the small amount of boilerplate required to mock methods with type arguments.