Add a callback method for the observer dynamically

I want to create a match that checks if an observer is viewing an observer.

I decided to add the method dynamically after_create

(if needed), save the model instance, and check if it is true that the observer instance received the call after_create

. Simplified version ( full version ):

RSpec::Matchers.define :be_observed_by do |observer_name|
  match do |obj|
    ...

    observer.class_eval do
      define_method(:after_create) {}
    end  

    observer.instance.should_receive(:after_create)

    obj.save(validate: false)

    ...

    begin
      RSpec::Mocks::verify  # run mock verifications
      true
    rescue RSpec::Mocks::MockExpectationError => e
      # here one can use #{e} to construct an error message
      false
    end
  end
end

      

This is not a job. An observer instance is not accepted after_create

.

But if I change the actual Observer code in app/models/user_observer.rb

like this

class UserObserver
  ...
  def after_create end
  ...
end

      

Works as expected.

What should I do to dynamically add a method after_create

to force a satellite after creation?

+3


source to share


1 answer


In short, this is because Rails hooks UserObserver callbacks to User events during initialization. If after_create

no callback is defined for UserObserver at this point , it will not be called, even if added later.

If you are interested in more details on how this observer initialization and connection to the wobserved class works, I posted a quick overview of the Observer implementation at the end. But before we get to that, here's a way to make your tests work. Now I'm not sure if you want to use this, and not sure why you decided to test the observer behavior in your application in the first place, but for the sake of completeness ...

Once done define_method(:after_create)

for the observer in your matches, inject an explicit call define_callbacks

(protected method, see walking through the Observer implementation below for what it does) on the observer instance. Here is the code:

observer.class_eval do
  define_method(:after_create) { |user| }
end
observer.instance.instance_eval do          # this is the added code
  define_callbacks(obj.class)               # - || -
end                                         # - || -

      

A quick overview of the Observer implementation.

Note. I am using "observer rails" gem sources (in Rails 4, observers were moved to an extra gem that is not installed by default). In your case, if you are on Rails 3.x, the implementation details may be different, but I believe the idea would be the same.

First, observer instantiation starts here: https://github.com/rails/rails-observers/blob/master/lib/rails/observers/railtie.rb#L24 . Basically, call ActiveRecord::Base.instantiate_observers

in ActiveSupport.on_load(:active_record)

, that is, when the ActiveRecord library is loaded.



In the same file, you can see how it takes a parameter config.active_record.observers

usually provided in config/application.rb

and passes it to the observers=

one defined here: https://github.com/rails/rails-observers/blob/master/lib/rails/observers/active_model /observing.rb#L38

But back to ActiveRecord::Base.instantiate_observers

. It just cycles through all the defined observers and calls instantiate_observer

for each of them. Implemented here instantiate_observer

: https://github.com/rails/rails-observers/blob/master/lib/rails/observers/active_model/observing.rb#L180 . Basically, it makes a call Observer.instance

(like a Singleton, the observer has one instance) that initializes that instance if it hasn't already.

This is how the Observer initialization looks like: https://github.com/rails/rails-observers/blob/master/lib/rails/observers/active_model/observing.rb#L340 . That is a challenge add_observer!

.

You can see add_observer!

along with and define_callbacks

which it calls here: https://github.com/rails/rails-observers/blob/master/lib/rails/observers/activerecord/observer.rb#L95 .

This method define_callbacks

goes through all the callbacks defined in your observer class (UserObserver) at the time and creates methods "_notify_#{observer_name}_for_#{callback}"

for the observable class (User) and registers them as triggering this event in the observable class (User, again).

In your case, it should have been a _notify_user_observer_for_after_create

method added as a after_create

callback for the user. Internally, this _notify_user_observer_for_after_create

will call update

in the UserObserver class, which in turn will call after_create

on UserObserver, and everything will work from there.

But in your case, it after_create

doesn't exist at UserObserver

Rails initialization time, so no method is created or registered for the callback User.after_create

. So no luck after that with catching it in your tests. This little secret is being solved.

+3


source