Expand description
Mocking / spying library based on lightweight function proxies.
Mocking in Rust is somewhat hard compared to object-oriented languages. Since there is no implicit / all-encompassing class hierarchy, Liskov substitution principle does not apply, making it generally impossible to replace an object with its mock. A switch is only possible if the object consumer explicitly opts in via parametric polymorphism or dynamic dispatch.
What do? Instead of trying to emulate mocking approaches from the object-oriented world,
this crate opts in for another approach, somewhat similar to remote derive from serde
.
Mocking is performed on function / method level, with each function conditionally proxied
to a mock that has access to function args and can do whatever: call the “real” function
(e.g., to spy on responses), maybe with different args and/or after mutating args;
substitute with a mock response, etc. Naturally, mock logic
can be stateful (e.g., determine a response from the predefined list; record responses
for spied functions etc.)
Overview
- Define the state to hold data necessary for mocking / spying and derive
Mock
for it. Requirements to the state are quite lax; it should be'static
andSend
. - Place
mock
attrs referencing the state on the relevant functions, methods and/or impl blocks. - Define mock logic as inherent methods of the mock state type. Such methods will be called with the same args as the original functions + additional first arg for the mock state reference. In the simplest case, each mocked function / method gets its own method with the same name as the original, but this can be customized.
- If the state needs to be mutated in mock logic, add a
#[mock(mut)]
attr on the state. In this case, the mock method will receive&
Mut
<Self>
wrapper as the first arg instead of&self
. If the mocked function / method isasync
, the mock implementation will receiveMockRef
<Self>
as the first arg. - If the mock logic needs to be shared across threads, add a
#[mock(shared)]
attr on the state. (By default, mocks are thread-local.) - Set the mock state in tests using
Mock::set_as_mock()
. Inspect the state during tests usingMockGuard::with()
and after tests usingMockGuard::into_inner()
.
Features and limitations
- Can mock functions / methods with a wide variety of signatures, including generic functions
(with not necessarily
'static
type params), functions returning non-'static
responses and responses with dependent lifetimes, such as infn(&str) -> &str
, functions withimpl Trait
args etc. - Can mock methods in
impl
blocks, including trait implementations. - Single mocking function can mock multiple functions, provided that they have compatible signatures.
- Whether mock state is shared across functions / methods, is completely up to the test writer.
Functions for the same receiver type / in the same
impl
block may have different mock states. - Mocking functions can have wider argument types than required from the signature of function(s) being mocked. For example, if the mocking function doesn’t use some args, they can be just replaced with unconstrained type params.
Downsides
- You still cannot mock types from other crates.
- Even if mocking logic does not use certain args, they need to be properly constructed, which, depending on the case, may defy the reasons behind using mocks.
- Very limited built-in matching / verifying (see
Answers
). With the chosen approach, it is frequently easier and more transparent to just usematch
statements. As a downside, if matching logic needs to be customized across tests, it’s (mostly) up to the test writer.
Crate features
shared
(Off by default)
Enables mocks that can be used across multiple threads.
Examples
Basics
use mimicry::{mock, CallReal, RealCallSwitch, Mock};
// Mock target: a standalone function.
#[cfg_attr(test, mock(using = "SearchMock"))]
fn search(haystack: &str, needle: char) -> Option<usize> {
haystack.chars().position(|ch| ch == needle)
}
// Mock state. In this case, we use it to record responses.
#[derive(Default, Mock, CallReal)]
struct SearchMock {
called_times: Cell<usize>,
switch: RealCallSwitch,
// ^ Stores the real / mocked function switch, thus allowing
// to call `Delegate` trait methods.
}
impl SearchMock {
// Mock implementation: an inherent method of the mock state
// specified in the `#[mock()]` macro on the mocked function.
// The mock impl receives same args as the mocked function
// with the additional context parameter that allows
// accessing the mock state and controlling mock / real function switches.
fn search(
&self,
haystack: &str,
needle: char,
) -> Option<usize> {
self.called_times.set(self.called_times.get() + 1);
match haystack {
"test" => Some(42),
short if short.len() <= 2 => None,
_ => {
let new_needle = if needle == '?' { 'e' } else { needle };
self.call_real().scope(|| search(haystack, new_needle))
}
}
}
}
// Test code.
let guard = SearchMock::default().set_as_mock();
assert_eq!(search("test", '?'), Some(42));
assert_eq!(search("?!", '?'), None);
assert_eq!(search("needle?", '?'), Some(1));
assert_eq!(search("needle?", 'd'), Some(3));
let recovered = guard.into_inner();
assert_eq!(recovered.called_times.into_inner(), 4);
Mock functions only get a shared reference to the mock state; this is because
the same state can be accessed from multiple places during recursive calls.
To easily mutate the state during tests, consider using the Mut
wrapper.
On impl blocks
The mock
attribute can be placed on impl blocks (including trait implementations)
to apply a mock to all methods in the block:
struct Tested(String);
#[mock(using = "TestMock")]
impl Tested {
fn len(&self) -> usize { self.0.len() }
fn push(&mut self, s: impl AsRef<str>) -> &mut Self {
self.0.push_str(s.as_ref());
self
}
}
#[mock(using = "TestMock", rename = "impl_{}")]
impl AsRef<str> for Tested {
fn as_ref(&self) -> &str {
&self.0
}
}
#[derive(Mock)]
struct TestMock { /* ... */ }
// Since we don't use partial mocking / spying, we indicate
// this with an empty `CheckRealCall` impl.
impl CheckRealCall for TestMock {}
impl TestMock {
fn len(&self, recv: &Tested) -> usize {
// ...
}
fn push<'s>(
&self,
recv: &'s mut Tested,
s: impl AsRef<str>,
) -> &'s mut Tested {
// ...
}
fn impl_as_ref<'s>(&self, recv: &'s Tested) -> &'s str {
// ...
}
}
What can(’t) be mocked?
struct Test;
impl Test {
#[mock(using = "CountingMock::count")]
fn do_something(&self) {}
#[mock(using = "CountingMock::count")]
fn lifetimes(&self) -> &str {
"what?"
}
#[mock(using = "CountingMock::count")]
fn generics<T: ToOwned>(value: &T) -> Vec<T::Owned> {
(0..5).map(|_| value.to_owned()).collect()
}
#[mock(using = "CountingMock::count")]
fn impl_methods(value: &impl AsRef<str>) -> &str {
value.as_ref()
}
}
impl Iterator for Test {
type Item = u8;
#[mock(using = "CountingMock::count")]
fn next(&mut self) -> Option<Self::Item> {
Some(42)
}
}
#[derive(Default, Mock)]
struct CountingMock(AtomicU32);
impl CheckRealCall for CountingMock {}
impl CountingMock {
// All functions above can be mocked with a single impl!
// This is quite extreme, obviously; in realistic scenarios,
// you probably wouldn't be able to unite mocks of functions
// with significantly differing return types.
fn count<T, R: Default>(&self, _: T) -> R {
self.0.fetch_add(1, Ordering::Relaxed);
R::default()
}
}
let guard = CountingMock::default().set_as_mock();
Test.do_something();
assert_eq!(Test.lifetimes(), "");
assert_eq!(Test.next(), None);
let count = guard.into_inner().0;
assert_eq!(count.into_inner(), 3);
Finally, async
functions can be mocked as well, although they require a bit more complex
setup. See MockRef
docs for examples.
Structs
AnswersSender
are timely consumed.Answers::channel()
.RefCell
) allowing to easily
mutate it in mock code.static
s with mock state.Traits
call_*
methods in this trait can be used
for partial mocking and spying.