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 ) => { ... };
}Expand description
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:
- const method (
&self) or mutable method (&mut self) - return value (
fn foo(&self) -> bool) or no return value (fn foo(&self)) - 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.