rspec-mocks icon indicating copy to clipboard operation
rspec-mocks copied to clipboard

Impossible to check that method is executed in a range times.

Open VitaliiLazebnyi opened this issue 4 years ago • 7 comments

Subject of the issue

According to the documentation methods, at_least and at_most can be chained to have range check: method is called between N and M times. (at least N times and not more then M times). But rspec-mocks actually checks the last condition and ignores previous ones.

Your environment

  • Ruby version: ruby 2.7.1p83 (2020-03-31 revision a0c7c23c9c) [x86_64-linux-gnu]
  • rspec-mocks version: rspec-mocks (3.7.0)

Steps to reproduce

  it "doesn't support double checks" do
    tmp = double('Test Variable')
    expect(tmp).to receive(:call).and_return(nil).at_least(1).at_most(2).times
  end

  it "doesn't support double checks" do
    tmp = double('Test Variable')
    expect(tmp).to receive(:call).and_return(nil).at_most(2).at_least(1).times
    tmp.call
    tmp.call
    tmp.call
  end

Expected behavior

Both tests should fail since one of the conditions is not followed.

Actual behavior

Both tests pass since the condition, which is at the end, overwrites the previous one.

VitaliiLazebnyi avatar Jun 25 '21 10:06 VitaliiLazebnyi

According to the documentation methods, at_least and at_most can be chained to have range check

I couldn't find this apart from self, to support further chaining. Surely, this is confusing, as the implementation disregards the previously set constraint and overwrites it.

What I would suggest:

  1. Untangle at_least/at_most/exactly by not using a common expected_received_count.
  2. Make exactly incompatible with the other two, add a failure message.
  3. Restrict using at_least/at_most more than once.

We can only afford 2 & 3 in a major version, but as we're nearing 4.0, it's possible to do so in 4-0-dev branch. The main branch would need to print a deprecation message when exactly used with at_least/at_most, or the latter two are set more than once.

I'd suggest taking a look at https://github.com/rspec/rspec-expectations/blob/43bf64b01f8356979ffbc373b2e81d2ab1389b29/lib/rspec/matchers/built_in/count_expectation.rb#L103 for inspiration.

Would you like to hack on it?

pirj avatar Jun 25 '21 12:06 pirj

Is it acceptable to have 2+3+add method .in_range(from..to)?

VitaliiLazebnyi avatar Jun 25 '21 13:06 VitaliiLazebnyi

Something like

expect(tmp).to receive(:call).and_return(nil).in_range(1..2).times

? 🤔

pirj avatar Jun 25 '21 20:06 pirj

Yes, something like it.

VitaliiLazebnyi avatar Jun 28 '21 01:06 VitaliiLazebnyi

is it acceptable to have 2+3+add method .in_range(from..to)?

Shortly: no.

The introduction of this new method is orthogonal to the fixes, and can be done separately. Adding a new method that would attempt to replace the existing malfunctioning/deceptive at_least/at_most is not a good option. I'd suggest addressing the problems we have now and introduce the new method after.

pirj avatar Jun 28 '21 07:06 pirj

There's one more issue I could spot:

    r = double
    expect(r).to receive(:foo).at_most(:once).and_return(1, 2)
    r.foo
    r.foo

surprisingly, this passes.

pirj avatar Jun 28 '21 15:06 pirj

This way it works for me to test how many times method is called:

 allow_any_instance_of(ClassName).to receive(:your_method)
 
 allow(ClassName).to receive(:find).and_return(your_instance) # if you need to test #update action
 put your_route_url(your_instance, subdomain: 'your_subdomain', host: '127.0.0.1.xip.io', type: 'your_type'), params: params # if you need to test #update action

 expect(your_instance).to have_received(:your_method).once

yaroslavrick avatar Jan 24 '24 18:01 yaroslavrick