Crate autospy

Crate autospy 

Source
Expand description

A test spy object library.

#[autospy] is a macro to create a spy from almost any trait. The spy can be used in unit tests as a stand-in for the real trait.

§Usage

  • Attribute your trait using #[autospy]
  • Set return values using set()
  • Check captured arguments
#[autospy::autospy]
trait MyTrait {
    fn foo(&self, argument: u32) -> u32;
}

fn call_with_ten(x: impl MyTrait) -> u32 {
    x.foo(10)
}

let spy = MyTraitSpy::default();
spy.foo.returns.set([20]);

assert_eq!(20, call_with_ten(spy.clone()));
assert_eq!([10], spy.foo.arguments);
The generated spy object and trait impl are #[cfg(test)] by default. To disable this see features.

It is recommended to use #[cfg_attr(test, autospy)], as well for all attributes discussed below, to make it transparent autospy is only expanded under test.

§Multiple arguments

Functions with multiple arguments are captured in a tuple.

#[autospy::autospy]
trait MyTrait {
    fn foo(&self, arg1: u32, arg2: String);
}

fn use_trait(x: impl MyTrait)  {
    x.foo(10, "hello!".to_string())
}

let spy = MyTraitSpy::default();
spy.foo.returns.set([()]);

use_trait(spy.clone());

assert_eq!([(10, "hello!".to_string())], spy.foo.arguments);

§Reference arguments

#[autospy] will automatically convert reference arguments into owned types when captured.

#[autospy::autospy]
trait MyTrait {
    fn foo(&self, argument: &str);
}

fn use_trait(x: impl MyTrait) {
    x.foo("hello!")
}

let spy = MyTraitSpy::default();
spy.foo.returns.set([()]);

use_trait(spy.clone());

assert_eq!(["hello!"], spy.foo.arguments);

§Reference returns

Functions that return non-mutable and mutable references are supported. If an explicit lifetime is not provided the lifetime of the reference will be the same as the spy.

#[autospy::autospy]
trait MyTrait {
    fn foo(&self) -> &str;
}

fn use_trait(x: &impl MyTrait) -> &str {
    x.foo()
}

let spy = MyTraitSpy::default();
spy.foo.returns.set(["hello!"]);

assert_eq!("hello!", use_trait(&spy));

§Ignore arguments

Arguments can be ignored using #[autospy(ignore)] if you do not wish to capture them in the spy.

#[autospy::autospy]
trait MyTrait {
    fn foo(&self, #[autospy(ignore)] ignored: &str, argument: &str);
}

fn use_trait(x: impl MyTrait) {
    x.foo("ignored!", "capture me!")
}

let spy = MyTraitSpy::default();
spy.foo.returns.set([()]);

use_trait(spy.clone());

assert_eq!(["capture me!"], spy.foo.arguments);

Arguments called _ will also be implicitly ignored.

#[autospy::autospy]
trait MyTrait {
    fn foo(&self, _: &str, argument: &str);
}

fn use_trait(x: impl MyTrait) {
    x.foo("ignored!", "capture me!")
}

let spy = MyTraitSpy::default();
spy.foo.returns.set([()]);

use_trait(spy.clone());

assert_eq!(["capture me!"], spy.foo.arguments);

§Associated types

An #[autospy(TYPE)] attribute can be applied to associated types to tell the spy how to capture them.

#[autospy::autospy]
trait MyTrait {
    #[autospy(String)]
    type Item;
    fn foo(&self, argument: Self::Item);
}

fn use_trait(x: impl MyTrait<Item=String>) {
    x.foo("hello!".to_string())
}

let spy = MyTraitSpy::default();
spy.foo.returns.set([()]);

use_trait(spy.clone());

assert_eq!(["hello!"], spy.foo.arguments);

§Generic associated types

Generic associated types are also supported via the #[autospy(TYPE)] attribute.

#[autospy::autospy]
trait LendingIterator {
    #[autospy(&'a str)]
    type Item<'a>
    where
        Self: 'a;

    fn next<'a>(&'a mut self) -> Self::Item<'a>;
}

fn use_trait<T>(lending_iterator: &mut T) -> &str
where
    T: for<'a> LendingIterator<Item<'a> = &'a str>,
{
    lending_iterator.next()
}

 let mut spy = LendingIteratorSpy::default();
 spy.next.returns.set(["hello!"]);

 assert_eq!("hello!", use_trait(&mut spy));

§External traits

External traits can be turned into a spy using #[autospy(external)], you will need to include the signatures for the external trait functions you want the spy to implement.

use std::io::Read;

#[autospy::autospy(external)]
trait Read {
    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize>;
}

fn use_trait(mut x: impl Read) -> std::io::Result<usize> {
    let mut buf = [];
    x.read(&mut buf)
}

let spy = ReadSpy::default();
spy.read.returns.set([Err(std::io::Error::other("read fails!"))]);

assert!(use_trait(spy).is_err());

§Returns attribute

Trait functions that return generics can have the return type specified using the #[autospy(returns = "TYPE")] attribute.

#[autospy::autospy]
trait MyTrait {
    #[autospy(returns = "String")]
    fn foo(&self) -> impl ToString;
}

fn use_trait(x: impl MyTrait) -> String {
    x.foo().to_string()
}

let spy = MyTraitSpy::default();
spy.foo.returns.set(["a string!".to_string()]);

assert_eq!("a string!", use_trait(spy));

§Static trait arguments

Trait functions that have generic arguments and are 'static will automatically be captured in a Box.

#[autospy::autospy]
trait MyTrait {
    fn foo(&self, argument: impl ToString + 'static);
}

fn use_trait(x: impl MyTrait) {
    x.foo("hello!")
}

let spy = MyTraitSpy::default();
spy.foo.returns.set([()]);

use_trait(spy.clone());

assert_eq!("hello!", spy.foo.arguments.take()[0].to_string())

§Generic traits

The spy will have the same generics as the trait definition.

#[autospy::autospy]
trait MyTrait<A: Copy, R> {
    fn foo(&self, argument: A) -> R;
}

fn use_trait(x: impl MyTrait<u32, String>) -> String {
    x.foo(10)
}

let spy = MyTraitSpy::<u32, String>::default();
spy.foo.returns.set(["hello!".to_string()]);

assert_eq!("hello!", use_trait(spy.clone()));

assert_eq!([10], spy.foo.arguments)

§Async traits

Async functions in traits are stable as of Rust 1.75; however, this did not include support for using traits containing async functions as dyn Trait. They can be used via the async_trait crate. #[autospy] is compatible with the #[async_trait] macro.

#[autospy] must come before #[async_trait].
use pollster::FutureExt as _;

#[autospy::autospy]
#[async_trait::async_trait]
trait MyTrait {
    async fn foo(&self, argument: &str);
}

async fn use_async_trait(x: impl MyTrait) {
    x.foo("hello async!").await
}

let spy = MyTraitSpy::default();
spy.foo.returns.set([()]);

use_async_trait(spy.clone()).block_on();

assert_eq!(["hello async!"], spy.foo.arguments)

If you are using an async trait your spy might not be used immediately, for instance it might be spawned in a task. You can use the recv() function on arguments to instruct the spy to wait asynchronously until the spy is used. recv() is enabled by the default feature async and, as an async function, will need to be called from within an async test.

use std::time::Duration;

#[autospy::autospy]
#[async_trait::async_trait]
trait MyTrait: Send + 'static {
    async fn foo(&self, argument: &str);
}

async fn use_async_trait(x: impl MyTrait) {
    tokio::task::spawn(async move {
        tokio::time::sleep(Duration::from_millis(100)).await;
        x.foo("async used after some time!").await;
    });
}

tokio::runtime::Runtime::new().unwrap().block_on(async {
    let spy = MyTraitSpy::default();
    spy.foo.returns.set([()]);

    use_async_trait(spy.clone()).await;
    // spy not used yet
    assert!(spy.foo.arguments.take().is_empty());
    // spy used after 100ms
    assert_eq!("async used after some time!", spy.foo.arguments.recv().await[0])
})

§Into attribute

If you wish to capture an argument as a different type, and it implements From you can use the #[autospy(into = "TYPE")] attribute on the argument.

use std::net::Ipv4Addr;

#[autospy::autospy]
trait MyTrait {
    fn foo(&self, #[autospy(into = "Ipv4Addr")] ip: [u8; 4]);
}

fn use_trait(x: impl MyTrait) {
    x.foo([192, 168, 0, 1])
}

let spy = MyTraitSpy::default();
spy.foo.returns.set([()]);

use_trait(spy.clone());

assert_eq!([Ipv4Addr::new(192, 168, 0, 1)], spy.foo.arguments)

§Into with attribute

If you wish to capture an argument as a different type, and it doesn’t implement From you can use the #[autospy(into = "TYPE", with = "FUNCTION")] attribute on the argument.

use std::string::FromUtf8Error;

#[autospy::autospy]
trait MyTrait {
    fn foo(&self, #[autospy(into = "Result<String, FromUtf8Error>", with = "String::from_utf8")] bytes: Vec<u8>);
}

fn use_trait(x: impl MyTrait) {
    x.foo(b"hello!".to_vec())
}

let spy = MyTraitSpy::default();
spy.foo.returns.set([()]);

use_trait(spy.clone());

assert_eq!([Ok("hello!".to_string())], spy.foo.arguments)

§Associated consts

An #[autospy(VALUE)] attribute can be applied to associated consts to set them in the spy. Alternatively, if no attribute is provided and the type has a Default that will be used.

#[autospy::autospy]
trait MyTrait {
    #[autospy(100)]
    const VALUE: u64;
    const DEFAULT: u64;
    fn foo(&self);
}

assert_eq!(100, MyTraitSpy::VALUE);
assert_eq!(0, MyTraitSpy::DEFAULT);

§Default trait implementations

If your trait has a default implementation for a function, an #[autospy(use_default)] attribute can be used on the method to tell the spy to use the default. Therefore, no spy values will be recorded for this function.

#[autospy::autospy]
trait MyTrait {
    #[autospy(20)]
    const VALUE: u64;
    #[autospy(use_default)]
    fn foo(&self) -> u64 {
        Self::VALUE + 100
    }
}

fn use_trait(x: impl MyTrait) -> u64 {
    x.foo()
}

assert_eq!(120, use_trait(MyTraitSpy::default()));

§Supertraits

Supertraits are supported through the supertrait! macro by putting the supertrait definition inside the macro. The supertrait must be in scope. If using autospy as a dev dependency you MUST mark the supertrait macro as #[cfg(test)].

use std::io::Read;

#[autospy::autospy]
trait MyTrait: Read {
    fn foo(&self) -> u64;
    autospy::supertrait! {
        trait Read {
            fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize>;
        }   
    }
}

fn use_trait(mut x: impl MyTrait) -> (u64, std::io::Result<usize>) {
    let mut buf = [];
    (x.foo(), x.read(&mut buf))
}

let spy = MyTraitSpy::default();
spy.foo.returns.set([1]);
spy.read.returns.set([Ok(0)]);

let result =  use_trait(spy);
assert_eq!(1, result.0);
assert_eq!(0, result.1.unwrap())

§Examples

For additional examples please see the examples.

§Features

  • test - makes the generated spy object and trait impl #[cfg(test)] - enabled by default.
  • async - enables additional async support features on the spy, if you are not using async traits you can safely disable this - enabled by default.

Macros§

supertrait
Allows the spy to use supertraits.

Structs§

Arguments
The captured arguments of a spy function.
Returns
The return values of a spy function.
SpyFunction
Captures arguments and holds return values.

Attribute Macros§

autospy
Automatically generate spy objects for traits.