0
class ExternalObject
  attr_accessor :external_object_attribute

  def update_external_attribute(options = {})
    self.external_object_attribute = [1,nil].sample
  end
end

class A
  attr_reader :my_attr, :external_obj

  def initialize(external_obj)
    @external_obj = external_obj
  end

  def main_method(options = {})
    case options[:key]
    when :my_key
      self.my_private_method(:my_key) do 
        external_obj.update_external_attribute(reevaluate: true)
      end
    else
      nil
    end
  end

  private
  def my_private_method(key)
     old_value = key
     external_object.external_object_attribute = nil
     yield
     external_object.external_object_attribute = old_value if external_object.external_object_attribute.nil?
  end
end

I want to test following for main_method when options[:key] == :my_key:

my_private_method is called once with argument :my_key and it has a block {external_obj.update_external_attribute(reevaluate: true) } , which calls update_external_attribute on external_obj with argument reevaluate: true once.

I'm able to test my_private_method call with :my_key argument once.

expect(subject).to receive(:my_private_method).with(:my_key).once

But how do I test the remaining part of the expectation?

Thank you

aridlehoover
  • 3,139
  • 1
  • 26
  • 24
Indyarocks
  • 643
  • 1
  • 6
  • 26

2 Answers2

0

It could be easier to answer your question if you post your test as well. The setup, the execution and asseriotns/expectations.

You can find a short answer in this older question.

You can find useful to read about yield matchers.

I would suggest to mock the ExternalObject if you already haven't. But I can't tell unless you post your actual test code.

0

I'm going to answer your question. But, then I'm going to explain why you should not do it that way, and show you a better way.

In your test setup, you need to allow the double to yield so that the code will fall through to your block.

RSpec.describe A do
  subject(:a) { described_class.new(external_obj) }

  let(:external_obj) { instance_double(ExternalObject) }

  describe '#main_method' do
    subject(:main_method) { a.main_method(options) }

    let(:options) { { key: :my_key } }

    before do
      allow(a).to receive(:my_private_method).and_yield
      allow(external_obj).to receive(:update_external_attribute)

      main_method
    end

    it 'does something useful' do
      expect(a)
        .to have_received(:my_private_method)
        .with(:my_key)
        .once

      expect(external_obj)
        .to have_received(:update_external_attribute)
        .with(reevaluate: true)
        .once
    end
  end
end

That works. The test passes. RSpec is a powerful tool. And, it will let you get away with that. But, that doesn't mean you should. Testing a private method is ALWAYS a bad idea.

Tests should only test the public interface of a class. Otherwise, you'll lock yourself into the current implementation causing the test to fail when you refactor the internal workings of the class - even if you have not changed the externally visible behavior of the object.

Here's a better approach:

RSpec.describe A do
  subject(:a) { described_class.new(external_obj) }

  let(:external_obj) { instance_double(ExternalObject) }

  describe '#main_method' do
    subject(:main_method) { a.main_method(options) }

    let(:options) { { key: :my_key } }

    before do
      allow(external_obj).to receive(:update_external_attribute)
      allow(external_obj).to receive(:external_object_attribute=)
      allow(external_obj).to receive(:external_object_attribute)

      main_method
    end

    it 'updates external attribute' do
      expect(external_obj)
        .to have_received(:update_external_attribute)
        .with(reevaluate: true)
        .once
    end
  end
end

Note that the expectation about the private method is gone. Now, the test is only relying on the public interface of class A and class ExternalObject.

Hope that helps.

aridlehoover
  • 3,139
  • 1
  • 26
  • 24