Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to stub class instantiated inside tested class in rspec

Tags:

ruby

rspec

I have problem stubbing external api, following is the example

require 'rspec'
require 'google/apis/storage_v1'

module Google
  class Storage
    def upload file
      puts '#' * 90
      puts "File #{file} is uploaded to google cloud"
    end
  end
end

class UploadWorker
  include Sidekiq::Worker

  def perform
    Google::Storage.new.upload 'test.txt'
  end
end


RSpec.describe UploadWorker do

  it 'uploads to google cloud' do
    google_cloud_instance = double(Google::Storage, insert_object: nil)
    expect(google_cloud_instance).to receive(:upload)
    worker = UploadWorker.new
    worker.perform
  end
end

I'm trying to stub Google::Storage class. This class is instantiated inside the object being tested. How can I verify the message expectation on this instance?

When I run above example, I get following output, and it seems logical, my double is not used by tested object

(Double Google::Storage).upload(*(any args))
           expected: 1 time with any arguments
           received: 0 times with any arguments

I'm new to Rspec and having hard time with this, any help will be appreciated.

Thanks!

like image 978
Rajesh Naik Avatar asked Jul 18 '18 11:07

Rajesh Naik


Video Answer


2 Answers

Reaching for DI is always a good idea (https://stackoverflow.com/a/51401376/299774) but there are sometimes reasons you can't so it, so here's another way to stub it without changing the "production" code.

1. expect_any_instance_of

it 'uploads to google cloud' do
  expect_any_instance_of(Google::Storage).to receive(:insert_object)
  worker = UploadWorker.new
  worker.perform
end

In case you just want to test that the method calls the method on any such objects.

2. bit more elaborated setup

In case you want to control or set up more expectations, you can do this

  it 'uploads to google cloud' do
    the_double = instance_double(Google::Storage)
    expect(Google::Storage).to receive(:new).and_return(the_double)
       # + optional `.with` in case you wanna assert stuff passed to the constructor
    expect(the_double).to receive(:insert_object)
    worker = UploadWorker.new
    worker.perform
  end

Again - Dependency Injection is clearer, and you should aim for it. This is presented as another possibility.

like image 71
Grzegorz Avatar answered Sep 20 '22 11:09

Grzegorz


I would consider reaching for dependency injection, such as:

class UploadWorker
  def initialize(dependencies = {})
    @storage = dependencies.fetch(:storage) { Google::Storage }
  end

  def perform
    @storage.new.upload 'test.txt'
  end
end

Then in the spec you can inject a double:

storage = double
expect(storage).to receive(...) # expection
worker = UploadWorker.new(storage: storage)
worker.perform

If using the initializer is not an option then you could use getter/setter method to inject the dependency:

def storage=(new_storage)
  @storage = new_storage
end

def storage
  @storage ||=  Google::Storage
end

and in the specs:

storage = double
worker.storage = storage
like image 32
Kris Avatar answered Sep 22 '22 11:09

Kris