Search code examples
unit-testingrspecautomated-testsrspec-railsrspec3

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 ?)


Solution

  • 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.