Cyril Duchon-Doris
Cyril Duchon-Doris

Reputation: 14039

Rspec - how to test that a nested service is instanciated appropriately and the appropriate method is called on this instance

Suppose I have a class that is supposed to call a service multiple times with different arguments

class MyExecutor
  def perform
    targets.each do |target|
      MyService.new(target).call
    end
  end
end

class MyService
  def initialize(target)
    @target = target
  end

  def call
    @target.do_something
  end
end

Assume I want to write a test on MyExecutor, generating data so that I have at least 2 targets, and I want to test that the service MyService is called appropriately on all targets.

When I had only one target, I could use expect_any_instance_of().to receive(:call) but then I was not really testing the instanciation with appropriate params, and then this syntax is deprecated (cf comment here)

describe MyExecutor
  context 'with one target'
    it 'calls the MyService appropriately'
      expect_any_instance_of(MyService).to receive(:call)
      MyExecutor.perform
    end
  end
end

Suppose I have multiple targets, how can I test that the MyService is instanciated twice, once with each relevant target, and that on each of those instanciated services, the call method is called appropriately What is the proper non-deprecated way to test this ?

Implicit question : (is this the right way to approach the problem ?)

Upvotes: 0

Views: 1961

Answers (2)

aridlehoover
aridlehoover

Reputation: 3615

In Rspec 3.8 syntax:

describe MyExecutor do
  subject(:executor) { described_class.new }

  describe '#perform' do
    subject(:perform) { executor.perform }

    let(:target1) { instance_double('target' }
    let(:target2) { instance_double('target' }
    let(:service1) { instance_double(MyService, call: true) }
    let(:service2) { instance_double(MyService, call: true) }

    before do 
      allow(MyExecutor).to receive(:targets).and_return([target1, target2])
      allow(MyService).to receive(:new).with(target1).and_return(service1)
      allow(MyService).to receive(:new).with(target2).and_return(service2)

      perform
    end

    it 'instantiates MyService once for each target' do
      expect(MyService).to have_received(:new).with(target1).ordered
      expect(MyService).to have_received(:new).with(target2).ordered
    end

    it 'calls MyService once for each target' do
      expect(service1).to have_received(:call)
      expect(service2).to have_received(:call)
    end
  end
end

Note that using .ordered allows you to specify the exact order of operations.

Note that doubling MyService .with a specific parameter allows you to control the return value for that specific parameter.

Upvotes: 1

Anthony
Anthony

Reputation: 15987

I think I'm understanding your need:

 describe MyExecutor do
   context 'with one target' do
     it 'calls the MyService appropriately' do
       target1 = double("TargetClass")
       target2 = double("TargetClass")
       allow(MyExecutor).to receive(:targets).and_return([target1, target2])

       service1 = double(MyService, call: nil)
       service2 = double(MyService, call: nil)
       expect(MyService).to receive(:new).with(target1).once.and_return(service1)
       expect(MyService).to receive(:new).with(target2).once.and_return(service2)

       MyExecutor.perform
     end
   end
 end

Upvotes: 1

Related Questions