Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).

- Provide expectations clearing for automock.
Ex: Useful to configure a general mock behavior through a helper function,
and then specialize a single method by clearing it's initial behavior.
Partially addresses issue [#283](https://github.com/asomers/mockall/issues/283)

Comment on lines +6 to +10
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- Provide expectations clearing for automock.
Ex: Useful to configure a general mock behavior through a helper function,
and then specialize a single method by clearing it's initial behavior.
Partially addresses issue [#283](https://github.com/asomers/mockall/issues/283)
## [ Unreleased ] - ReleaseDate
### Added
- Provide expectations clearing for a mock object's expectations. Useful to configure general mock behavior through a helper function, and then specialize a single method by clearing it's initial behavior.
([#676](https://github.com/asomers/mockall/pull/676))

## [ 0.14.0 ] - 2025-11-22

### Added
Expand Down
45 changes: 45 additions & 0 deletions mockall/tests/automock_struct.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
//! automocking a struct
#![deny(warnings)]

use std::panic;
use mockall::*;

pub struct SimpleStruct {}
Expand All @@ -20,3 +21,47 @@ fn returning() {
.returning(|x| i64::from(x) + 1);
assert_eq!(5, mock.foo(4));
}

#[test]
fn clear() {
let mut mock = MockSimpleStruct::new();
mock.expect_foo()
.returning(|x| i64::from(x) + 1);
assert_eq!(5, mock.foo(4));

mock.expect_foo().returning(|x| i64::from(x) + 2);
assert_eq!(5, mock.foo(4));

mock.clear_foo();

mock.expect_foo().returning(|x| i64::from(x) + 2);
assert_eq!(6, mock.foo(4));
}

#[test]
fn clear_and_expect() {
let mut mock = MockSimpleStruct::new();
mock.expect_foo()
.returning(|x| i64::from(x) + 1);
assert_eq!(5, mock.foo(4));

mock.expect_foo().returning(|x| i64::from(x) + 2);
assert_eq!(5, mock.foo(4));

mock.clear_and_expect_foo().returning(|x| i64::from(x) + 2);
assert_eq!(6, mock.foo(4));
}

#[test]
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just like in automock_trait, you should use #[should_panic] here.

fn calling_foo_without_expectation_after_clear() {
let mut mock = MockSimpleStruct::new();
mock.expect_foo()
.returning(|x| i64::from(x) + 1);
assert_eq!(5, mock.foo(4));

mock.clear_foo();
let result = panic::catch_unwind(|| {
mock.foo(4)
});
assert!(result.is_err());
}
46 changes: 46 additions & 0 deletions mockall/tests/automock_trait.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
//! automocking a trait
#![deny(warnings)]

use std::panic;
use mockall::*;

#[automock]
Expand All @@ -16,3 +17,48 @@ fn returning() {
.returning(|x| x + 1);
assert_eq!(5, mock.foo(4));
}

#[test]
fn clear() {
let mut mock = MockSimpleTrait::new();
mock.expect_foo()
.returning(|x| x + 1);
assert_eq!(5, mock.foo(4));

mock.expect_foo().returning(|x| x + 2);
assert_eq!(5, mock.foo(4));

mock.clear_foo();

mock.expect_foo().returning(|x| x + 2);
assert_eq!(6, mock.foo(4));
}

#[test]
fn clear_and_expect() {
let mut mock = MockSimpleTrait::new();
mock.expect_foo()
.returning(|x| x + 1);
assert_eq!(5, mock.foo(4));

mock.expect_foo().returning(|x| x + 2);
assert_eq!(5, mock.foo(4));

mock.clear_and_expect_foo().returning(|x| x + 2);
assert_eq!(6, mock.foo(4));
}

#[test]
fn calling_foo_without_expectation_after_clear() {
let mut mock = MockSimpleTrait::new();
mock.expect_foo()
.returning(|x| x + 1);
assert_eq!(5, mock.foo(4));

mock.clear_foo();
let result = panic::catch_unwind(|| {
mock.foo(4)
});
assert!(result.is_err());
}

Comment on lines +51 to +64
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rust already provides us with a great way to assert that something panics. Let's use it.

Suggested change
#[test]
fn calling_foo_without_expectation_after_clear() {
let mut mock = MockSimpleTrait::new();
mock.expect_foo()
.returning(|x| x + 1);
assert_eq!(5, mock.foo(4));
mock.clear_foo();
let result = panic::catch_unwind(|| {
mock.foo(4)
});
assert!(result.is_err());
}
#[test]
#[should_panic(expected = "@nobodie should fill in this string")]
fn calling_foo_without_expectation_after_clear() {
let mut mock = MockSimpleTrait::new();
mock.expect_foo()
.returning(|x| x + 1);
assert_eq!(5, mock.foo(4));
mock.clear_foo();
mock.foo(4)
}

123 changes: 123 additions & 0 deletions mockall_derive/src/mock_function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -722,6 +722,119 @@ impl MockFunction {
)
}

/// Generate code for the clear_ method
///
/// # Arguments
///
/// * `modname`: Name of the parent struct's private module
// Supplying modname is an unfortunately hack. Ideally MockFunction
// wouldn't need to know that.
pub fn clear(&self, modname: &Ident)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method contains much duplicated code from the expect method just above, and from the clear_and_expect method just below. That's a shame. But all three methods are called from exactly the same places. What about combining them into a single method that generates code for three mock methods? That could save some SLOC.

-> impl ToTokens
Comment on lines +732 to +733
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
pub fn clear(&self, modname: &Ident)
-> impl ToTokens
pub fn clear(&self, modname: &Ident) -> impl ToTokens

{
let attrs = AttrFormatter::new(&self.attrs)
.doc(false)
.format();
let name = self.name();
let clear_ident = format_ident!("clear_{}", name);
let funcname = &self.sig.ident;
let (_, tg, _) = if self.is_method_generic() {
&self.egenerics
} else {
&self.call_generics
}.split_for_impl();
let (ig, _, wc) = self.call_generics.split_for_impl();
let mut wc = wc.cloned();
if self.is_method_generic() && (self.return_ref || self.return_refmut) {
// Add Senc + Sync, required for downcast, since Expectation
// stores an Option<#owned_output>
send_syncify(&mut wc, self.owned_output.clone());
}
let tbf = tg.as_turbofish();
let vis = &self.call_vis;

let substruct_obj = if let Some(trait_) = &self.trait_ {
let ident = format_ident!("{trait_}_expectations");
quote!(#ident.)
} else {
quote!()
};
let docstr = format!("Clear the [`Expectation`]({}/{}/struct.Expectation.html) array for the mocked `{}` method",
modname, self.inner_mod_ident(), funcname);
quote!(
#[doc = #docstr]
#(#attrs)*
#vis fn #clear_ident #ig(&mut self)
{
self.#substruct_obj #name.clear #tbf();
}
)
}

/// Generate code for the clear_and_expect_ method
///
/// # Arguments
///
/// * `modname`: Name of the parent struct's private module
/// * `self_args`: If supplied, these are the
/// AngleBracketedGenericArguments of the self type of the
/// trait impl. e.g. The `T` in `impl Foo for Bar<T>`.
// Supplying modname is an unfortunately hack. Ideally MockFunction
// wouldn't need to know that.
pub fn clear_and_expect(&self, modname: &Ident, self_args: Option<&PathArguments>)
-> impl ToTokens
{
let attrs = AttrFormatter::new(&self.attrs)
.doc(false)
.format();
let name = self.name();
let clear_and_expect_intent = format_ident!("clear_and_expect_{}", name);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let clear_and_expect_intent = format_ident!("clear_and_expect_{}", name);
let clear_and_expect_ident = format_ident!("clear_and_expect_{}", name);

let expectation_obj = self.expectation_obj(self_args);
let funcname = &self.sig.ident;
let (_, tg, _) = if self.is_method_generic() {
&self.egenerics
} else {
&self.call_generics
}.split_for_impl();
let (ig, _, wc) = self.call_generics.split_for_impl();
let mut wc = wc.cloned();
if self.is_method_generic() && (self.return_ref || self.return_refmut) {
// Add Senc + Sync, required for downcast, since Expectation
// stores an Option<#owned_output>
send_syncify(&mut wc, self.owned_output.clone());
}
let tbf = tg.as_turbofish();
let vis = &self.call_vis;

#[cfg(not(feature = "nightly_derive"))]
let must_use = quote!(#[must_use =
"Must set return value when not using the \"nightly\" feature"
]);
#[cfg(feature = "nightly_derive")]
let must_use = quote!();

let substruct_obj = if let Some(trait_) = &self.trait_ {
let ident = format_ident!("{trait_}_expectations");
quote!(#ident.)
} else {
quote!()
};
let docstr = format!("Clear the [`Expectation`]({}/{}/struct.Expectation.html) array, before creating one for mocking the `{}` method",
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let docstr = format!("Clear the [`Expectation`]({}/{}/struct.Expectation.html) array, before creating one for mocking the `{}` method",
let docstr = format!("Clear the existing [`Expectation`]({}/{}/struct.Expectation.html) array, and add a new expectation for mocking the `{}` method",

modname, self.inner_mod_ident(), funcname);
quote!(
#must_use
#[doc = #docstr]
#(#attrs)*
#vis fn #clear_and_expect_intent #ig(&mut self)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
#vis fn #clear_and_expect_intent #ig(&mut self)
#vis fn #clear_and_expect_ident #ig(&mut self)

-> &mut #modname::#expectation_obj
#wc
{
self.#substruct_obj #name.clear #tbf();
self.#substruct_obj #name.expect #tbf()
}
)
}

/// Return the name of this function's expecation object
fn expectation_obj(&self, self_args: Option<&PathArguments>)
-> impl ToTokens
Expand Down Expand Up @@ -1256,6 +1369,11 @@ impl ToTokens for CommonExpectationsMethods<'_> {
&mut self.0[__mockall_l - 1]
}

#v fn clear(&mut self)
{
self.0.clear();
}

#v const fn new() -> Self {
Self(Vec::new())
}
Expand Down Expand Up @@ -2576,6 +2694,11 @@ impl ToTokens for StaticGenericExpectations<'_> {
.unwrap()
.expect()
}

#v fn clear #ig (&mut self)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like it will clear expectations for all combinations of generic arguments. Is that intended? I would think that it would be more useful to clear them for only one specific set of generic arguments.

{
self.store.clear();
}
}
).to_tokens(tokens)
}
Expand Down
10 changes: 10 additions & 0 deletions mockall_derive/src/mock_item_struct.rs
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,14 @@ impl ToTokens for MockItemStruct {
.filter(|meth| !meth.is_static())
.map(|meth| meth.expect(modname, None))
.collect::<Vec<_>>();
let clears = self.methods.0.iter()
.filter(|meth| !meth.is_static())
.map(|meth| meth.clear(modname))
.collect::<Vec<_>>();
let clear_and_expects = self.methods.0.iter()
.filter(|meth| !meth.is_static())
.map(|meth| meth.clear_and_expect(modname, None))
.collect::<Vec<_>>();
let method_checkpoints = self.methods.checkpoints();
let new_method = self.new_method();
let priv_mods = self.methods.priv_mods();
Expand Down Expand Up @@ -350,6 +358,8 @@ impl ToTokens for MockItemStruct {
#(#calls)*
#(#contexts)*
#(#expects)*
#(#clears)*
#(#clear_and_expects)*
/// Validate that all current expectations for all methods have
/// been satisfied, and discard them.
pub fn checkpoint(&mut self) {
Expand Down
17 changes: 17 additions & 0 deletions mockall_derive/src/mock_trait.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,21 @@ impl MockTrait {
meth.expect(modname, Some(path_args))
}
}).collect::<Vec<_>>();
let clears = self.methods.iter()
.filter(|meth| !meth.is_static())
.map(|meth|
meth.clear(modname)
).collect::<Vec<_>>();
Comment on lines +160 to +162
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.map(|meth|
meth.clear(modname)
).collect::<Vec<_>>();
.map(|meth| meth.clear(modname)).collect::<Vec<_>>();

let clear_and_expects = self.methods.iter()
.filter(|meth| !meth.is_static())
.map(|meth| {
if meth.is_method_generic() {
// Specific impls with generic methods are TODO.
meth.clear_and_expect(modname, None)
} else {
meth.clear_and_expect(modname, Some(path_args))
}
}).collect::<Vec<_>>();
let trait_path = &self.trait_path;
let self_path = &self.self_path;
let types = &self.types;
Expand All @@ -169,6 +184,8 @@ impl MockTrait {
#(#impl_attrs)*
impl #ig #self_path #wc {
#(#expects)*
#(#clears)*
#(#clear_and_expects)*
#(#contexts)*
}
)
Expand Down