Crate mimicry

source ·
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

  1. 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 and Send.
  2. Place mock attrs referencing the state on the relevant functions, methods and/or impl blocks.
  3. 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.
  4. 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 is async, the mock implementation will receive MockRef<Self> as the first arg.
  5. If the mock logic needs to be shared across threads, add a #[mock(shared)] attr on the state. (By default, mocks are thread-local.)
  6. Set the mock state in tests using Mock::set_as_mock(). Inspect the state during tests using MockGuard::with() and after tests using MockGuard::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 in fn(&str) -> &str, functions with impl 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 use match 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

Answers for a function call.
Guard ensuring that answers sent from an AnswersSender are timely consumed.
Sender part of a channel created by Answers::channel().
Exclusive guard to set the mock state without an attached state.
Exclusive guard to set the mock state.
Reference to a mock state used when mocking async functions / methods.
A lightweight wrapper around the state (essentially, a RefCell) allowing to easily mutate it in mock code.
Guard for the real / mock implementation switch.
Switch between real and mocked implementations.
Sharedshared
Wrapper around Mock state that provides cross-thread synchronization.
Wrapper that allows creating statics with mock state.
Thread-local mock state wrapper.

Traits

Controls delegation to real impls. The provided call_* methods in this trait can be used for partial mocking and spying.
Checks whether it is necessary to delegate to real impl instead of the mock.
State of a mock.

Attribute Macros

Injects mocking logic into a function / method.

Derive Macros

Derives the CallReal trait for a struct allowing to switch to real implementations for partial mocking or spying.
Derives the Mock trait for a type, allowing to use it as a state for mocking.