【问题标题】:How to properly mock itnernal services in RSpec?如何正确模拟 RSpec 中的内部服务?
【发布时间】:2019-09-24 17:32:36
【问题描述】:

我想学习如何在其他类中正确地模拟对象调用,例如我有这个控制器操作:

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

服务类如下所示。

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

我不想在这个控制器操作和所有其他方法中调用实际的 Action::PartsShow 服务,服务 + 工作者,因为这会使测试非常慢。我想要的是测试是否正在调用此服务并模拟其余服务。我不想在我的测试中调用它们,我想模拟它们。

我的测试是这样的:

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

你能告诉我如何正确地模拟它吗?我读过 RSpec 模拟和存根,但我对此感到困惑。非常感谢您的帮助。

【问题讨论】:

    标签: ruby rspec rspec-rails


    【解决方案1】:

    假设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
    

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

    【讨论】:

    • 但是你看,我什至不想到达那里,我想模拟整个Action::PartsShow.new(show_params, current_user) 服务以避免调用它。我只是希望它被调用并返回一个值,但我根本不想调用它。
    • 如果您这样做并且 Action::PartsShow 的功能发生变化,您的规范将不会通知您该控制器的核心功能存在问题。您仍然可以使用 allow(Action::PartsShow).to receive(:new).with(show_params, current_user) { service } 执行此操作,以确保使用该方法和那些参数(您应该在规范中使用 let vars 定义)的任何调用将返回您使用 let var 定义的特定内容。它的基本语法相同,并且比 allow_any_instance_of 更安全。 let(:service) { double(Action::PartShow, call: part) }
    【解决方案2】:

    使用 rspec-mocks gem,您可以使用allow_any_instance_of。通常,这部分位于before 块中。

    其实Action::PartsShow负责加载一个part,所以不需要泄露callpart这两个实例方法。您可以通过从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
    

    参考

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

    【讨论】:

    • 你说得对,我会重构这个。这是我被另一家公司接管的遗留代码。事实上,在我看来,代码有异味,可以进一步重构。谢谢!
    • 工人呢?它会在测试中调用吗? ExternalData 服务呢?它也会被调用吗?
    • 如果你存根Action::PartsShow#call,它的函数体被跳过,只返回你在and_return(...)中指定的内容。
    • 返回success怎么样?例如,可能有类似if Import.perform(import: @import).success?
    • allow_any_instance_of 应该是最后的手段。甚至还有一个 rubocop 规则:docs.rubocop.org/rubocop-rspec/cops_rspec.html#rspecanyinstance
    猜你喜欢
    • 2013-11-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-10-23
    • 1970-01-01
    相关资源
    最近更新 更多