【问题标题】:Globally mock method calling external API全局模拟调用外部 API 的方法
【发布时间】:2021-10-31 18:37:49
【问题描述】:

背景:我正在实现一个已有 6 年历史的 Rails 项目,从那时起就没有接触过框架。因此,我正在重新学习很多东西。

我正在尝试了解模拟需要同步完成的 API 调用的最佳方法。 Order 有一个 Invoice,并且 Invoice必须从外部服务获取参考。没有发票,订单就毫无用处。

以下是该应用程序的简单版本。 Order 模型是应用程序的核心。

开放式问题:

  • 在 spec_helper.rb 中全局模拟 SDK 的最佳做法是什么?其中将包含我的allow_any_instance_of(InvoiceServiceSdk)
  • 我有一个 Order 工厂,在我的测试中几乎无处不在。但是我很困惑我是否也可以在 Invoice 工厂中循环。 FactoryBot 现在对我来说很陌生。
# app/models/order.rb
class Order < ApplicationRecord
  has_one :invoice, autosave: true

  before_create :build_invoice

  def build_invoice
    self.invoice = Invoice.new
  end
end
# app/models/invoice.rb
class Invoice < ApplicationRecord
  belongs_to :order

  before_create :generate

  def generate
    invoice_service = InvoiceServiceSdk.new
    self.external_id = invoice_service.fetch
  end
end
# app/models/invoice_service_sdk.rb
require 'uri'
require 'net/http'

class InvoiceServiceSdk
  def fetch
    uri = URI('https://example.com/') # Real HTTP request
    res = Net::HTTP.get_response(uri)
    SecureRandom.urlsafe_base64       # "ID" that API "provides"
  end
end
# spec/models/order.rb
require 'rails_helper'

RSpec.describe Order, type: :model do
  before do
    allow_any_instance_of(InvoiceServiceSdk).to receive(:fetch).and_return('super random external invoice ID')
  end
  context "new order + invoice" do
    it {
      o = Order.new
      o.save
      expect(o.invoice.external_id).to eq 'super random external invoice ID'
    }
  end
end

【问题讨论】:

  • 您应该将其缩小到一个问题。 allow_any_instance_of 被认为是代码异味,RSpec 团队不建议在遗留代码之外使用它。您应该在您的服务对象上创建一个可以被存根的工厂方法。从几天前看到这个答案到一个类似的问题stackoverflow.com/a/68993724/544825
  • 可以使用 webmock 和 VCR gems 来存根 HTTP。
  • @max 我在整个测试过程中都在使用 WebMock 和 VCR。在 spec_helper 中 Stub 请求是不是很臭?
  • 我真的鼓励你创建一个关于 FactoryBot 的单独问题。
  • 工厂模式对我来说是新的。我在全球范围内使用 WebMocking 端点。

标签: ruby-on-rails ruby rspec factory-bot


【解决方案1】:

rspec-mocks documentation 不鼓励使用allow_any_instance

  • rspec-mocks API 是为单个对象实例设计的,但此功能对整个对象类都有效。因此,存在一些语义上令人困惑的边缘情况。例如,在 expect_any_instance_of(Widget).to receive(:name).twice 中,不清楚一个特定的实例是否应该接收 name 两次,或者是否预期两个接收总数。 (是前者。)
  • 使用此功能通常是一种设计味道。可能是您的测试试图做的太多,或者被测对象太复杂。

您可以通过向服务对象添加工厂方法来完全避免它:

class MyService
  def intialize(**kwargs)
    @options = kwargs
  end

  def call
    do_something_awesome(@options[:foo])
  end

  def self.call(**kwargs)
    new(**kwargs).call
  end
end

allow(MyService).to recieve(:call).and_return([:foo, :bar, :baz])

在 spec_helper 中 Stub 请求是不是很臭?

不一定。您可以通过如上所示重构代码并存根工厂方法来避免一些开销。它还使您的存根不会耦合到服务对象的内部工作。

我会更担心这样一个事实,即这段代码通过使用服务对象做一件正确的事情,然后通过在模型回调中调用它立即取消它。

【讨论】:

  • 我通常将 HTTP 调用放入一个单独类型的对象中,我会调用 API 客户端,它只执行 HTTP 并规范化响应,这是唯一允许知道的对象外部 API。例如,如果您想使用 API 并从中创建模型实例,您将同时创建服务对象和客户端类(或多个类)。
猜你喜欢
  • 2023-03-19
  • 1970-01-01
  • 2022-10-24
  • 2019-11-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-06-21
  • 1970-01-01
相关资源
最近更新 更多