onlynone
onlynone

Reputation: 8339

Verifying rspec mocks before the test is over

It seems like the standard way to use rspec mocks in a test case is to do something like this:

class MyTest
  def setup
    super
    ::RSpec::Mocks.setup(self)
  end

  def teardown
    super
    begin
      ::RSpec::Mocks.verify
    ensure
      ::RSpec::Mocks.teardown
    end
  end

  test "something"
    foo = MyFoo.new
    expect(foo).to receive(:bar).and_return(42)
    ret = SomeClass.call_bar(foo)

    assert_equal(42, ret)
  end
end

That works okay. But if SomeClass.call_bar used the return of foo.bar as the return, and something was wrong with the code such that foo.bar was never called, then I only receive a failure due to the assert_equal(42, ret) line. I don't see any error like:

RSpec::Mocks::MockExpectationError: (foo).bar
    expected: 1 time
    received: 0 times

If I remove the assert_equal(42, ret) line, then I do get the rspec expectation error. But I want to verify both things, that foo.bar was called and the final return was 42. It's more important to know that foo.bar wasn't called since that's the source of the reason that 42 wasn't returned.

If I'm expecting something like: expect(foo).not_to receive(:bar), then I do get that expectation error right at the source of the call, not later during the teardown.

Now, I can do something like put ::RSpec::Mocks.verify just before the call to assert_equal, but this doesn't feel right. I'm also not sure if I should be cleaning up the mocks at this point or not.

Is there some syntax like:

  test "something"
    foo = MyFoo.new
    ret = nil

    expect(foo).to receive(:bar).and_return(42).during do
      ret = SomeClass.call_bar(foo)
    end

    assert_equal(42, ret)
  end

So that the verification happens immediately after the block passed to during? Or maybe if you have multiple doubles, you could do something like:

    expect(dbl1).to receive(:one)
    expect(dbl2).to receive(:two)
    expect(dbl3).to receive(:three)

    verify(dbl1, dbl2, dbl3).during do
      my_code
    end

Upvotes: 1

Views: 806

Answers (3)

Greg
Greg

Reputation: 6649

I believe that what you need is to aggregate failures https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/aggregating-failures

In a "normal" setup, any error aborts the test and no later assertions are checked.

Upvotes: 1

Chris Heald
Chris Heald

Reputation: 62688

You're looking for rspec spies.

Spies are an alternate type of test double that support this pattern by allowing you to expect that a message has been received after the fact, using have_received.

You create a partial double out of your foo with allow(...).to receive, then can assert reception of the the message:

test "something"
  foo = MyFoo.new
  allow(foo).to receive(:bar).and_return(42)
  ret = SomeClass.call_bar(foo)
  expect(foo).to have_received(:bar)
  assert_equal(42, ret)
end

Upvotes: 3

onlynone
onlynone

Reputation: 8339

I don't think there's any built-in way to do it, but if you add the following class:

class VerifyDuring
  def initialize(test, objects)
    @test = test
    @objects = objects
  end

  def during
    yield
  ensure
    begin
      @objects.each do |object|
        RSpec::Mocks.proxy_for(object).verify
      end
    rescue Exception => e
      @test.flunk e
    end
  end
end

And the following method to your test class:

  def verify(*objects)
    VerifyDuring.new(self, objects)
  end

You can do this:

    verify(dbl1, dbl2, dbl3).during do
      my_code
    end

Upvotes: -2

Related Questions