首页 > 解决方案 > How to properly mock itnernal services in RSpec?

问题描述

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.

标签: rubyrspecrspec-rails

解决方案


假设find_part调用Part.find(id),您可以添加:

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

确保记录查找始终返回您的测试对象。我还建议稍微修改一下您的规范:

RSpec.describe PartController, type: :request do
  subject { response }
  
  describe '#show' do
    let(:request)  { get api_v1_part_path(part.id) }
    # If you insist on mocking the object, might as well use build_stubbed
    let(:part)     { build_stubbed(:part) } 
    let(:json)     { JSON.parse(response.body).deep_symbolize_keys }
    let(:expected) {
      {
        part: {
          id: parts.id,
          # ...
        }
      }
    }

    before do
      # This is the recommended way to mock an object
      allow(Part).to receive(:find).with(part.id).and_return(part)
      request
    end

    # Validate response status
    # https://relishapp.com/rspec/rspec-rails/docs/matchers/have-http-status-matcher
    # If you are using shoulda matchers - it works bc subject is the response
    it { should have_http_status(:success) }
    # otherwise
    it { expect(response).to have_http_status(:success) }

    # Validate response body
    it { expect(json).to eq(expected) }
  end
end

如果您的项目有路径助手,我还建议使用它们而不是路径字符串。


推荐阅读