JayTarka
JayTarka

Reputation: 571

Mock a method only once

Is there a way to mock a method once and only once per expectation before replacing the original mocked method?

I thought something like this would work (notice the once)

class Klass
    def self.meth
        'baz'
    end
end

describe Klass do
    subject{ described_class.meth }
    before{ allow(described_class).to receive(:meth).once.and_return('foo') }
    it{ is_expected.to eq 'foo' }
    context 'throwing in a context just to test' do
        it{ is_expected.to eq 'foo' }
        it{ is_expected.to eq 'foo' }
        it{ is_expected.to eq 'foo' }
        it 'only mocks once' do
            expect(subject).to eq 'foo'
            expect(subject).to eq 'baz' # this is the key
        end # pass
    end
end

Unfortunately I get this error:

   (Klass (class)).meth(no args)
       expected: 1 time with any arguments
       received: 2 times

I would have expected to have got that failure if I said expect(Klass).to receive(:meth).once rather than the more lenient allow.

I'm wondering how I can mock once and only once per expectation.

Upvotes: 4

Views: 10066

Answers (1)

Paul Fioravanti
Paul Fioravanti

Reputation: 16793

It's perhaps a bit unintuitive, but you can do this by specifying different return values for multiple calls of Klass.meth.

In your case, you can stub the first call to Klass.meth with 'foo', and then stub every other call to Klass.meth with the original implementation of the method. That looks like this:

allow(described_class).to receive(:meth).and_return('foo', described_class.meth)

The next thing we'd need to change in your test is to not use subject in the final test, because it is memoising the value returned when Klass.meth gets called the first time (which is why all the other tests that use subject will still pass), and hence making the second expectation in the it 'only mocks once' test fail. Instead, we can just call the method directly in each spec:

class Klass
  def self.meth
    'baz'
  end
end

describe Klass do
  subject { described_class.meth }

  before do
    allow(described_class).to \
      receive(:meth).and_return('foo', described_class.meth)
  end

  it { is_expected.to eq 'foo' }

  context 'throwing in a context just to test' do
    it { is_expected.to eq 'foo' }
    it { is_expected.to eq 'foo' }
    it { is_expected.to eq 'foo' }

    it 'only mocks once' do
      expect(described_class.meth).to eq 'foo'
      expect(described_class.meth).to eq 'baz'
    end # pass
  end
end

Upvotes: 8

Related Questions