How to properly mock itnernal services in RSpec?

混江龙づ霸主 提交于 2021-01-28 14:41:36

问题


I would like to learn how to properly mock object calls inside other classes, foor example I have this controller action:

def show
 service = Action::PartsShow.new(show_params, current_user)
 service.call
 render json: service.part, root: :part, serializer: PartSerializer, include: '**',
        scope: {current_user: current_user}
end

The service class looks like this.

module Action
  class PartsShow < PartsShowBase
    def find_part
      ...
    end
  end
end

module Action
  class PartsShowBase
    attr_reader :part

    def initialize(params, current_user)
      @params = params
      @current_user = current_user
    end

    def call
      find_part
      reload_part_availability
      reload_part_price if @current_user.present?
    end

    private

    def reload_part_availability
      ReloadPartAvailabilityWorker.perform_async(part.id)
    end

    def reload_part_price
      ExternalData::LauberApi::UpdatePrices.new(@current_user, [part]).execute
    end
  end
end

I don't want to call the actual Action::PartsShow service inside this controller action and all other methods, services + the worker because this makes the test very slow. What I want is to test if this service is being called and mock the rest of the services. I don't want to call them in my tests, I want to mock them.

My test looks like this:

RSpec.describe PartController, type: :request do
  describe 'GET #show' do
    let(:part) { create(:part) }

    subject { get "/api/v1/parts/#{part.id}" }

    expect(response_body).to eq(200)
    # ...
  end
end

Could you show me how to properly mock it? I've read about RSpec mocks and stubs but I am confused about it. I would appreciate your help.


回答1:


With rspec-mocks gem, you can use allow_any_instance_of. Usually, this part lies in before block.

In fact,Action::PartsShow is responsible for loading a part, so there is no need to leak two instance methods: call and part. You can simplify it through returning the part from call.

module Action
  class PartsShowBase
    #attr_reader :part

    def call
      find_part # assign @part
      reload_part_availability
      reload_part_price if @current_user.present?
      @part
    end
    ...
end
RSpec.describe PartController, type: :request do
  before :all do
    allow_any_instance_of(Action::PartsShow).to receive(:call).and_return(returned_part)
  end

Reference

https://relishapp.com/rspec/rspec-mocks/v/3-5/docs/working-with-legacy-code/any-instance




回答2:


Assuming that find_part calls Part.find(id), you can add:

before do
  allow(Part).to receive(:find).with(part.id) { part }
end

to ensure that the record lookup always returns your test object. I also suggest reworking your spec a bit:

RSpec.describe PartController, type: :request do
  describe 'GET #show' do
    let(:request) { get "/api/v1/parts/#{part.id}" }
    let(:part)    { create(:part) }

    it '200s' do
      request
      expect(response).to have_http_status(:success)
    end
  end
end

If your project has path helpers, I would also recommend using those instead of the path string.



来源:https://stackoverflow.com/questions/58085529/how-to-properly-mock-itnernal-services-in-rspec

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!