Skip to content

Commit

Permalink
Allow mocking some methods with generic non-static arguments
Browse files Browse the repository at this point in the history
Add a #[mockall::concretize] attribute.  When set on a function or
method, its generic expectations will be turned into trait objects.

But it only works for function arguments that are pure generic types or
a few basic combinations:
* T
* &T
* &mut T
* &[T]

Issue #217
  • Loading branch information
asomers committed Sep 20, 2022
1 parent 6ec6c40 commit f81342d
Show file tree
Hide file tree
Showing 6 changed files with 592 additions and 43 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ This project adheres to [Semantic Versioning](http://semver.org/).

## [ Unreleased ] - ReleaseDate

## Added

- Added `#[mockall::concretize]`, which can be used to mock some generic
methods that have non-`'static` generic parameters. It works by turning the
generic arguments into trait objects for the expectation.
([#408](https://github.com/asomers/mockall/pull/408))

## Changed

- Raised MSRV to 1.45.0 because futures-task did.
Expand Down
80 changes: 72 additions & 8 deletions mockall/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@
//! * [`impl Trait`](#impl-trait)
//! * [`Mocking structs`](#mocking-structs)
//! * [`Generic methods`](#generic-methods)
//! * [`Methods with generic lifetimes`](#methods-with-generic-lifetimes)
//! * [`Generic traits and structs`](#generic-traits-and-structs)
//! * [`Associated types`](#associated-types-1)
//! * [`Multiple and inherited traits`](#multiple-and-inherited-traits)
Expand Down Expand Up @@ -673,12 +672,15 @@
//!
//! ## Generic methods
//!
//! Generic methods can be mocked, too. Effectively each generic method is an
//! infinite set of regular methods, and each of those works just like any other
//! regular method. The expect_* method is generic, too, and usually must be
//! called with a turbofish. The only restrictions on mocking generic methods
//! are that all generic parameters must be `'static`, and generic lifetime
//! parameters are not allowed.
//! Mocking generic methods is possible, but the exact process depends on
//! whether the parameters are `'static`, non-`'static`, or lifetimes.
//!
//! ### With static parameters
//!
//! With fully `'static` parameters, the mock method is generic and so is its
//! expect_* method. The expect_* method usually must be called with a
//! turbofish. Expectations set with different generic parameters operate
//! completely independently of one another.
//!
//! ```
//! # use mockall::*;
Expand All @@ -697,7 +699,15 @@
//! assert_eq!(-5, mock.foo(5i8));
//! ```
//!
//! ## Methods with generic lifetimes
//! ### With non-`static` type parameters
//!
//! Mocking methods with non-`'static` type parameters is harder. The way
//! Mockall does it is by turning the generic parameters into trait objects
//! before evaluating expectations. This makes the expect_* method concrete,
//! rather than generic. It also comes with many restrictions. See
//! [`#[concretize]`](attr.concretize.html) for more details.
//!
//! ### With generic lifetimes
//!
//! A method with a lifetime parameter is technically a generic method, but
//! Mockall treats it like a non-generic method that must work for all possible
Expand Down Expand Up @@ -1257,6 +1267,60 @@ pub mod examples;
/// to choose your own name for the mock structure.
pub use mockall_derive::automock;

/// Decorates a method or function to tell Mockall to treat its generic arguments
/// as trait objects when creating expectations.
///
/// This allows users to use non-`'static` generic parameters, which otherwise
/// can't be mocked. The downsides of using this attribute are:
///
/// * Mockall can't tell if a parameter isn't `'static`, so you must annotate
/// such methods with the `#[mockall::concretize]` attribute.
/// * Generic methods will share expectations for all argument types. That is,
/// you won't be able to do `my_mock.expect_foo::<i32>(...)`.
/// * It can't be used on methods with a closure argument (though this may be
/// fixable).
/// * Concretized methods' expectations may only be matched with `.withf` or
/// `.withf_st`, not `.with`.
/// * It only works for parameters that can be turned into a trait object.
/// may be fixable).
/// * Mockall needs to know how to turn the function argument into a trait
/// object. Given a generic parameter `T`, currently supported patterns are:
/// - `T`
/// - `&T`
/// - `&mut T`
/// - `&[T]`
///
/// # Examples
/// ```
/// # use std::path::Path;
/// # use mockall::{automock, concretize};
/// #[automock]
/// trait Foo {
/// #[mockall::concretize]
/// fn foo<P: AsRef<Path>>(&self, p: P);
/// }
///
/// # fn main() {
/// let mut mock = MockFoo::new();
/// mock.expect_foo()
/// .withf(|p| p.as_ref() == Path::new("/tmp"))
/// .return_const(());
/// mock.foo(Path::new("/tmp"));
/// # }
/// ```
///
/// NB: This attribute must be imported with its canonical name. It won't work
/// otherwise!
/// ```compile_fail
/// use mockall::concretize as something_else;
/// #[mockall::automock]
/// trait Foo {
/// #[something_else]
/// fn foo<T>(&self, t: T);
/// }
/// ```
pub use mockall_derive::concretize;

/// Manually mock a structure.
///
/// Sometimes `automock` can't be used. In those cases you can use `mock!`,
Expand Down
50 changes: 50 additions & 0 deletions mockall/tests/automock_concretize.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// vim: tw=80
//! #[concretize] works with #[automock], too.
#![deny(warnings)]

use mockall::*;
use std::path::{Path, PathBuf};

#[automock]
trait Foo {
#[concretize]
fn foo<P: AsRef<std::path::Path>>(&self, x: P);
}

#[automock]
pub mod mymod {
#[mockall::concretize]
pub fn bang<P: AsRef<std::path::Path>>(_x: P) { unimplemented!() }
}

mod generic_arg {
use super::*;

#[test]
fn withf() {
let mut foo = MockFoo::new();
foo.expect_foo()
.withf(|p| p.as_ref() == Path::new("/tmp"))
.times(3)
.return_const(());
foo.foo(Path::new("/tmp"));
foo.foo(PathBuf::from(Path::new("/tmp")));
foo.foo("/tmp");
}
}

mod module {
use super::*;

#[test]
fn withf() {
let ctx = mock_mymod::bang_context();
ctx.expect()
.withf(|p| p.as_ref() == Path::new("/tmp"))
.times(3)
.return_const(());
mock_mymod::bang(Path::new("/tmp"));
mock_mymod::bang(PathBuf::from(Path::new("/tmp")));
mock_mymod::bang("/tmp");
}
}
130 changes: 130 additions & 0 deletions mockall/tests/mock_concretize.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// vim: tw=80
//! Some generic methods with non-`'static` generic parameters can be mocked by
//! transforming the function arguments into trait objects.
#![deny(warnings)]

use mockall::*;
use std::path::{Path, PathBuf};

trait AsRefMut<T: ?Sized>: AsRef<T> + AsMut<T> {}
impl<Q, T> AsRefMut<T> for Q where Q: AsRef<T> + AsMut<T>, T: ?Sized {}

mock! {
Foo {
#[mockall::concretize]
fn foo<P: AsRef<std::path::Path>>(&self, x: P);

#[mockall::concretize]
fn boom<P>(&self, x: P) where P: AsRef<std::path::Path>;

#[mockall::concretize]
fn bang<P: AsRef<std::path::Path>>(x: P);

#[mockall::concretize]
fn boomref<P: AsRef<std::path::Path>>(&self, x: &P);

#[mockall::concretize]
fn boom_mutref<T: AsRefMut<str>>(&self, x: &mut T);

#[mockall::concretize]
fn boomv<P>(&self, x: &[P]) where P: AsRef<std::path::Path>;
}
}

mod generic_arg {
use super::*;

#[test]
fn withf() {
let mut foo = MockFoo::new();
foo.expect_foo()
.withf(|p| p.as_ref() == Path::new("/tmp"))
.times(3)
.return_const(());
foo.foo(Path::new("/tmp"));
foo.foo(PathBuf::from(Path::new("/tmp")));
foo.foo("/tmp");
}
}

mod where_clause {
use super::*;

#[test]
fn withf() {
let mut foo = MockFoo::new();
foo.expect_boom()
.withf(|p| p.as_ref() == Path::new("/tmp"))
.times(3)
.return_const(());
foo.boom(Path::new("/tmp"));
foo.boom(PathBuf::from(Path::new("/tmp")));
foo.boom("/tmp");
}
}

mod mutable_reference_arg {
use super::*;

#[test]
fn withf() {
let mut foo = MockFoo::new();
foo.expect_boom_mutref()
.withf(|p| p.as_ref() == "/tmp")
.once()
.returning(|s| s.as_mut().make_ascii_uppercase());
let mut s = String::from("/tmp");
foo.boom_mutref(&mut s);
assert_eq!(s, "/TMP");
}
}

mod reference_arg {
use super::*;

#[test]
fn withf() {
let mut foo = MockFoo::new();
foo.expect_boomref()
.withf(|p| p.as_ref() == Path::new("/tmp"))
.times(3)
.return_const(());
foo.boomref(&Path::new("/tmp"));
foo.boomref(&PathBuf::from(Path::new("/tmp")));
foo.boomref(&"/tmp");
}
}

mod slice {
use super::*;

#[test]
fn withf() {
let mut foo = MockFoo::new();
foo.expect_boomv()
.withf(|v|
v[0].as_ref() == Path::new("/tmp") &&
v[1].as_ref() == Path::new("/mnt")
).times(3)
.return_const(());
foo.boomv(&[Path::new("/tmp"), Path::new("/mnt")]);
foo.boomv(&[PathBuf::from("/tmp"), PathBuf::from("/mnt")]);
foo.boomv(&["/tmp", "/mnt"]);
}
}

mod static_method {
use super::*;

#[test]
fn withf() {
let ctx = MockFoo::bang_context();
ctx.expect()
.withf(|p| p.as_ref() == Path::new("/tmp"))
.times(3)
.return_const(());
MockFoo::bang(Path::new("/tmp"));
MockFoo::bang(PathBuf::from(Path::new("/tmp")));
MockFoo::bang("/tmp");
}
}
Loading

0 comments on commit f81342d

Please sign in to comment.