3

An action of a Rails controller makes an instance of a helper class (say SomeService), which performs some work and returns a results, something along the lines of:

def create
  ...
  result = SomeService.new.process
  ...
end

I want to stub what SomeService#process returns.

My question is - how do I do this?

The following works:

allow_any_instance_of(SomeService).to receive(:process).and_return('what I want')

However, the rspec-mock documentation discourages the use of allow_any_instance_of for the reasons states here:

The rspec-mocks API is designed for individual object instances, but this feature operates on entire classes of objects. As a result there are some semantically confusing edge cases. For example in expect_any_instance_of(Widget).to receive(:name).twice it isn't clear whether each specific instance is expected to receive name twice, or if two receives total are expected. (It's the former.)

Using this feature is often a design smell. It may be that your test is trying to do too much or that the object under test is too complex.

It is the most complicated feature of rspec-mocks, and has historically received the most bug reports. (None of the core team actively use it, which doesn't help.)

I think the idea is to do something like this:

some_service = instance_double('SomeService')
allow(some_service).to receive(:process).and_return('what I want')

However, how do I make the controller use the double and not make a new instance of SomeService?

Alexander Popov
  • 23,073
  • 19
  • 91
  • 130

2 Answers2

3

I usually do something like this.

let(:fake_service) { your double here or whatever }

before do
  allow(SomeService).to receive(:new).and_return(fake_service)
  # this might not be needed, depending on how you defined your `fake_service`
  allow(fake_service).to receive(:process).and_return(fake_results)
end
Sergio Tulentsev
  • 226,338
  • 43
  • 373
  • 367
  • What about if the `SomeService` class only call a class method. Something like `SomeService.some_class_method`, and we need to test that `some_class_method` was executed? – MatayoshiMariano Aug 02 '17 at 19:56
  • @MatayoshiMariano: yes, what about it? – Sergio Tulentsev Aug 02 '17 at 20:12
  • @SergioTulentsev For example, how can I test that the `new` method was called on `SomeService`? The only way that I could achive that is creating a method `some_service_class` in the controller class that returned `SomeService` and then stubbing that method to return the spy `allow_any_instance_of(SomeController).to(receive(:some_service_class).and_return(service_class_spy))`, and then in the test `expect(service_class_spy).to have_received(:new)`. I don't like this way, I think that is unclean. Is there a better way? PS: Ask me anything, may be I was not clear. – MatayoshiMariano Aug 02 '17 at 20:28
  • @MatayoshiMariano just `expect(SomeService).to receive(:some_class_method)`? No need for spies or anything – Sergio Tulentsev Aug 03 '17 at 06:19
  • @SergioTulentsev for some reason that does not work for me. To add more information, the SomeService class is a worker from [Shoryuken](https://github.com/phstc/shoryuken) – MatayoshiMariano Aug 03 '17 at 12:56
0

My suggestion is to remodel the way you interface with your Service Object:

class SomeService
  def self.call(*args)
    new(*args).tap(&:process)
  end

  def initialize(*args)
    # do stuff here
  end

  def process
    # do stuff here
  end

  def success?
    # optional method, might make sense according to your use case
  end
end

Since this is a project-wide convention, we know every .call returns the service object instance, which we query for things such as #success?, #error_messages, etcettera (largely dependent on your use cases).

When testing clients of this class, we should only verify they call the class method .call with the correct params, which is as straightforward as mocking the returned value.

The unit tests for this class method should attest it: - calls .new with the proper params; - calls #process on the created instance; - returns the created instance (not the result of process).

Having a class method as main point of entry of your service object interface favors flexibility. Both #initialize and #process could be made private, but I prefer not to for testing purposes.

Samuel Brandão
  • 376
  • 2
  • 4