petRUShka
petRUShka

Reputation: 10162

Add callback method to observer dynamically

I want to create matcher that test whether a model is watched by observer.

I decided to dynamically add method after_create (if necessary), save instance of model and check is it true that observer instance received an after_create call. 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

It wasn't work. No instance of observer is received after_create call.

But If I modify actual code of Observer in app/models/user_observer.rb like this

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

It works as expected.

What should I do to add after_create method dynamically to force trigger observer after create?

Upvotes: 3

Views: 1466

Answers (1)

moonfly
moonfly

Reputation: 1820

In short, this behavior is due to the fact that Rails hooks up UserObserver callbacks to User events at the initialization time. If the after_create callback is not defined for UserObserver at that time, it will not be called, even if later added.

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

After you do define_method(:after_create) for observer in your matcher insert the explicit call to define_callbacks (a protected method; see walkthrough through the Observer implementatiin below on what it does) on 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 brief walk-through through the Observer implementation.

Note: I'm using the "rails-observers" gem sources (in Rails 4 observers were moved to an optional gem, which by default is not installed). In your case, if you are on Rails 3.x, the details of implementation may be different, but I believe the idea will be the same.

First, this is where the observers' instantiation is launched: 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), i.e. when the ActiveRecord library is loaded.

In the same file you can see how it takes the config.active_record.observers parameter normally provided in the config/application.rb and passes it to the observers= 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 defined observers and calls instantiate_observer for each of them. Here is where the instantiate_observer is implemented: https://github.com/rails/rails-observers/blob/master/lib/rails/observers/active_model/observing.rb#L180. Basically, it makes a call to Observer.instance (as a Singleton, an observer has a single instance), which will initialize that instance if that was not done yet.

This is how Observer initialization looks like: https://github.com/rails/rails-observers/blob/master/lib/rails/observers/active_model/observing.rb#L340. I.e. a call to add_observer!.

You can see add_observer!, together with and define_callbacks that it calls, here: https://github.com/rails/rails-observers/blob/master/lib/rails/observers/activerecord/observer.rb#L95.

This define_callbacks method goes through all the callbacks defined in your observer class (UserObserver) at that time and creates "_notify_#{observer_name}_for_#{callback}" methods for the observed class (User), and register them to be called on that event in the observed class (User, again).

In your case, it should have been _notify_user_observer_for_after_create method added as after_create callback to User. Inside, that _notify_user_observer_for_after_create would call update on the UserObserver class, which in turn would call after_create on UserObserver, and all would work from there.

But, in your case after_create doesn't exist in UserObserver during Rails initialization, so no method is created and registered for User.after_create callback. Thus, no luck after that with catching it in your tests. That little mystery is solved.

Upvotes: 3

Related Questions