mockall icon indicating copy to clipboard operation
mockall copied to clipboard

Add ability to reset / clear existing expectations?

Open Sushisource opened this issue 3 years ago • 11 comments

Hello, thanks so much for this library, super useful.

I have test setup code that will set a bunch of default expectations on mocks, but then some tests would like to alter the default expectations after the mock is built. Right now, if I just make another expect_foo call, that'll create an additional expectation, where I'd like it to actually replace the existing one. A nice way to do this might be something like a remove_expectations_foo method. Would this be a reasonable feature to add?

Thanks!

Sushisource avatar Jun 04 '21 18:06 Sushisource

Note that expectations are evaluated in LIFO order. So if you set an expectation in your setup code and another one in a test case, the latter will be evaluated first. That means the former doesn't usually cause any problems, unless there is a .times on it or something. If you do have problems, I would suggest refactoring your setup strategy so as not to create expectations that you don't expect to fulfill.

asomers avatar Jun 04 '21 18:06 asomers

@asomers Hmm, the docs say the opposite about ordering: https://docs.rs/mockall/0.9.1/mockall/index.html#matching-multiple-calls ? It appears to be FIFO behavior to me.

And my use case is indeed using .times. I could change the setup strategy, but it'd be quite a bit more complicated than being able to wipe an existing expectation. Is your concern about having too many methods on the generated mock?

Sushisource avatar Jun 07 '21 16:06 Sushisource

Sorry; yes you're right it uses FIFO order. Have you considered just removing .times from your setup? In my own projects, the only methods I expect from the setup code are boilerplate stuff that could be called any number of times with the same result. Any method that must have a fixed number of calls I expect from the test method.

asomers avatar Jun 20 '21 14:06 asomers

It's important to me to verify the number of calls is correct. I happen to have some pretty complex logic involved in setting up the expectations, which is why it's useful to have this common setup that works the majority of the time, but occasionally a test has reason to want to overwrite one of the method's expectations.

So, certainly possible to restructure how they're set up to accommodate that, but I figure some functionality like this is probably a useful tool to have in any case if it's not too much of an imposition on the library.

Sushisource avatar Jul 02 '21 16:07 Sushisource

this is also something that would be useful for us. we're mocking some static methods, which have global expectations. everything works great if all the tests pass. but, one test failing can bleed into another if the test fails prior to all the expectations being hit.

TomPridham avatar Jul 08 '22 17:07 TomPridham

I have the issue of needing to change the return in the middle of a test. Also, it would be great to be able to set a catch all.

For example, if you are taking an index to a collection, you might want most of the output to be the default but only a few to be set. Currently, I don't know of a way to set the default in the setup. This makes my tests messy because I have call a function in the test after setting the other expectations that finalizes the setup with a default as the last expectation for a function.

If I later want to add a new expectation, I have to recreate it from scratch each time I change anything and include everything I did before but with a change.

Some way to overwrite expectations would be wonderful and having a way for a default set early would be awesome too. How about an option to be LIFO instead of FIFO? I think this might solve both.

MikeColeGuru avatar Oct 21 '22 21:10 MikeColeGuru

Well, @MikeColeGuru the straightforward way to have a "default" expectation that is also overridden by a "special case" expectation is to use the matchers, and rely on FIFO ordering. For example,

let mut mock = MockMyCollection::new();
mock.expect_get()
    .with(eq(4))
    .return_const(Some(5));
mock.expect_get()
    .with(any())    // "any" is really just documentation.  You could leave this line out
    .return_const(None);

In this setup, calling mock.get(4) will return Some(5), but calling it with any other value will return None. That said, I'm warming up to the idea of editting existing expectations. We just need to decide on what would be the best API. Can you suggest something?

asomers avatar Oct 21 '22 21:10 asomers

I would love to set the default in the setup.

fn setup() ->Rc<RefCell<MockMyCollection>> {
  let mock = Rc::new(RefCell::new(MockMyCollection::new()));
  mock.borrow_mut()
      .expect_get()
      .with(any())    // "any" is really just documentation.  You could leave this line out
      .return_const(None);

  mock
}

#[test]
fn test_something() {
  let mock = setup();

  mock.borrow_mut()
      .expect_get()
      .with(eq(4))
      .return_const(Some(5));

    // do something with it
}

But the above doesn't work because the default is what you get for everything. So, I end up having to call setup + new expectations many times for each time I change something because any has to be last and sorta locks it all in place.

I would like the API to be able to overwrite what you set in the normal way you use it. There might be a remove_expect after specifying which one you want through the normal methods.

// create it
mock.expect_foo()
  .with(eq(5))
  .return_const(7);

// edit it  
mock.expect_foo()
  .with(eq(5))
  .return_const(8);
  
// remove it
mock.expect_foo()
  .with(eq(5))
  .remove_expect();
  

MikeColeGuru avatar Oct 21 '22 22:10 MikeColeGuru

Alternatively ...

// create it
mock.expect_foo()
  .with(eq(5))
  .return_const(7);

// edit it  
mock.expect_foo()
  .with(eq(5))
  .return_const(8);
  
// remove it
mock.remove_foo()
  .with(eq(5))

// remove all expects for foo
mock.remove_foo()

MikeColeGuru avatar Oct 21 '22 22:10 MikeColeGuru

Another API that could make sense would be something like:

let mock = setup();
let expectations = mock.take_expectations();
let updated_expectations = expectations.filter(|e| e.call_func == MyMockTypeCall::Foo);
mock.set_expectations(updated_expectations)

Here take_expectations returns something like Iterator<Item=MyMockTypeExpectation>. You could also have an expectations_mut that gives mut references for editing.

Where a MyMockTypeExpectation is generated by the mocking code, and looks something like:

struct MyMockTypeExpectation {
    matchers: Vec<Box<dyn Predicate>>,
    call_type: MyMockTypeCall,
    times: Option<usize>,
    // etc, I suppose most of this probably already exists in generated Expectation types. 
    //  All that really matters here is exposing that stuff.
}
enum MyMockTypeCall {
    Foo,
    Bar
}

This lets you manipulate all the existing expectations with as much power as you could ever want. I wouldn't want to have the "edit" style API described in @MikeColeGuru's example because that seems to overlap with the existing FIFO concept of adding new expectations. The remove APIs make sense to me, though.

Sushisource avatar Oct 27 '22 21:10 Sushisource

I would love to use mocked traits with defaults/fallbacks in conjunction with the rstest crate as a fixture. Currently, this is not really possible because, as discussed in this thread, the expectations are FIFO. This sadly means that having a "catch-all" fixture first would prevent adding more specific expectations for a test later.

This could be solved if we could configure the expectations to be LIFO instead.

Plebshot avatar Dec 15 '23 21:12 Plebshot