Attribute Macro unimock::unimock

source ·
#[unimock]
Expand description

Autogenerate mocks for all methods in the annotated traits, and impl it for Unimock.

Mock generation happens by declaring a new MockFn-implementing struct for each method.

Example

use unimock::*;

#[unimock(api=Trait1Mock)]
trait Trait1 {
    fn a(&self) -> i32;
    fn b(&self) -> i32;
}

#[unimock]
trait Trait2 {
    fn c(&self) -> i32;
}

fn sum(obj: impl Trait1 + Trait2) -> i32 {
    obj.a() + obj.b() + obj.c()
}

fn test() {
    // Unimock now implements both traits:
    sum(Unimock::new(())); // note: panics at runtime!

    // Mock a single method (still panics, because all 3 must be mocked:):
    sum(Unimock::new(Trait1Mock::a.next_call(matching!()).returns(0)));
}

Unmocking

Unmocking of a mocked function means falling back to a true implementation.

A true implementation must be a standalone function, not part of a trait, where the first parameter is generic (a self-replacement), and the rest of the parameters are identical to MockFn::Inputs:

#[unimock(unmock_with=[my_original(self, a)])]
trait DoubleNumber {
    fn double_number(&self, a: i32) -> i32;
}

// The true implementation is a regular, generic function which performs number doubling!
fn my_original<T>(_: T, a: i32) -> i32 {
    a * 2
}

The unmock feature makes sense when the reason to define a mockable trait is solely for the purpose of inversion-of-control at test-time: Release code need only one way to double a number.

Standalone functions enables arbitrarily deep integration testing in unimock-based application architectures. When unimock calls the true implementation, it inserts itself as the generic first parameter. When this parameter is bounded by traits, the original fn is given capabilities to call other APIs, though only indirectly. Each method invocation happening during a test will invisibly pass through unimock, resulting in a great level of control. Consider:

#[unimock(api=FactorialMock, unmock_with=[my_factorial(self, input)])]
trait Factorial {
    fn factorial(&self, input: u32) -> u32;
}

// will it eventually panic?
fn my_factorial(f: &impl Factorial, input: u32) -> u32 {
    f.factorial(input - 1) * input
}

assert_eq!(
    120,
    // well, not in the test, at least!
    Unimock::new(
        FactorialMock::factorial.stub(|each| {
            each.call(matching!((input) if *input <= 1)).returns(1_u32); // unimock controls the API call
            each.call(matching!(_)).unmocked();
        })
    )
    .factorial(5)
);

Arguments

The unimock macro accepts a number of comma-separated key-value configuration parameters:

  • #[unimock(api=#ident)]: Export a mocking API as a module with the given name
  • #[unimock(api=[method1, method2, ..]): Instead of generating a module, generate top-level mock structs for the methods in the trait, with the names of those structs passed with array-like syntax in the same order as the methods appear in the trait definition.
  • #[unimock(unmock_with=[a, b, _]): Given there are e.g. 3 methods in the annotated trait, uses the given paths as unmock implementations. The functions are assigned to the methods in the same order as the methods are listed in the trait. A value of _ means no unmock support for that method.
  • #[unimock(prefix=path)]: Makes unimock use a different path prefix than ::unimock, in case the crate has been re-exported through another crate.