1

Scenario

Have a race case where concurrency can cause a duplicate key error. Take for example:

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

The Test

Now, to test the race case (based on the answer to Simulating race conditions in RSpec unit tests) we just need to monkey patch before_create_customer_by_external_id to call create_customer_from_external_id.

The Question

How can you do this without overriding the whole class and getting a "method not found" error?

Community
  • 1
  • 1
Ryan
  • 641
  • 5
  • 17

2 Answers2

3

After some digging, I came up with the following solution:

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

I verified that it was hitting the race case by using a code coverage tool and debug statements.

Happy to know if there's a cleaner way here.

Edit 2020-04-24

Per the comment, we should undef this method so it doesn't affect subsequent tests. Ref: https://medium.com/@scottradcliff/undefining-methods-in-ruby-eb7fba21f63f

I did not verify this, as I no longer have this test suite. Please let me know if it does/does not work.

Ryan
  • 641
  • 5
  • 17
2

A step on from monkey patching the class is to create an 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

This is not so very different to your solution but keeps the monkeypatch isolated to that context block.

You may not need the custom routes block - rspec sets up some dummy routes for the rest methods (edit, show, index etc)

If this context is inside a describe ControllerToOverride block then the argument to controller is optional, unless you have turned off config.infer_base_class_for_anonymous_controllers

Frederick Cheung
  • 83,189
  • 8
  • 152
  • 174
  • I get an "undefined method `controller' for # (NoMethodError)" with this. Does this actually extend (with proper inheritance) or replace the controller? – Ryan Mar 26 '15 at 11:44
  • this creates an anonymous subclass of the controller. You can only call this if you're in a spec that is tagged as being a controller spec. – Frederick Cheung Mar 26 '15 at 11:46