你怎么能在 rspec 中猴子修补控制器?

How can you monkey patch a controller in rspec?

场景

有一个并发可能导致重复键错误的竞争案例。举个例子:

def before_create_customer_by_external_id
end

def create_customer_from_external_id(external_id = nil)
  @customer = current_account.customers.create!(external_id: external_id || @external_id)
end

def set_new_or_old_customer_by_external_id
  if @customer.blank?
    before_create_customer_by_external_id
    create_customer_from_external_id
  end
rescue ActiveRecord::RecordInvalid => e
  raise e unless Customer.external_id_exception?(e)
  @customer = current_account.customers.find_by_external_id(@external_id)
end

测试

现在,为了测试竞争情况(基于 Simulating race conditions in RSpec unit tests 的答案),我们只需要猴子补丁 before_create_customer_by_external_id 来调用 create_customer_from_external_id.

问题

如何在不覆盖整个 class 并得到 "method not found" 错误的情况下执行此操作?

经过一番挖掘,我想出了以下解决方案:

context 'with race condition' do
  it 'should hit race case and do what is expected' do
    ControllerToOverride.class_eval do
      def before_create_new_customer_by_external_id
        create_customer_from_external_id
      end
    end

    # ...expect...

    ControllerToOverride.class_eval do
      undef before_create_new_customer_by_external_id
    end
  end
end

我通过使用代码覆盖工具和调试语句验证了它是否符合竞争情况。

很高兴知道这里是否有更简洁的方法。

编辑 2020-04-24

根据评论,我们应该undef此方法,以免影响后续测试。参考:https://medium.com/@scottradcliff/undefining-methods-in-ruby-eb7fba21f63f

我没有验证这一点,因为我不再有这个测试套件。如果 does/does 不起作用,请告诉我。

猴子修补 class 的下一步是创建一个 anonymous subclass:

context "with race condition" do
   controller(ControllerToOverride) do
      def before_create_customer_by_external_id
      end
   end

   it "should deal with it " do
     routes.draw { # define routes here }
     ...
   end
end

这与您的解决方案并没有太大不同,但将 monkeypatch 与该上下文块隔离开来。

您可能不需要自定义路由块 - rspec 为其余方法(编辑、显示、索引等)设置一些虚拟路由

如果此上下文在 describe ControllerToOverride 块内,则控制器的参数是可选的,除非您已关闭 config.infer_base_class_for_anonymous_controllers