We are a software consultancy based in Berlin, Germany. We deliver
high quality web apps in short timespans.

Upstream Agile GmbH

Using and Testing ActiveRecord/Rails Observers

October 27, 2007 by alex

social feed

We recently introduced a new feature in autoki called the social feed. It’s basically a yellow box displaying any events on the platform relevant to the current user, like a friend has posted a new photo, or a new interesting car was uploaded. The data model behind this is pretty straightforward, we have a FeedEvent class and all kinds of subclasses, e.g. a MessageReceivedEvent. Each event belongs to a user and an event source, in this example the user would be the user who received the message and the event source would be the message itself. For each user, we simply display all the events that belong to him or her.

Now the question was this: How do we create these events? The most straightforward way would probably have been to create them in the models, so the Message model would have an after_create callback that created the event. What we didn’t like about this solution was that we would put a whole bunch of logic into the models that didn’t really belong there. Why would a Message care if there was some kind of event feed? Plus these events would be all around in our unit tests and make the bloated and probably sloooow (again). So we wanted to use the observer pattern to remove the creation of the event from the models.

For observers in a Rails app you basically have two choices waiting there for you: the ruby Observable mixin and the ActiveRecord::Observer class. We didn’t have much time (as usual) and only took a very short look at both and quickly decided to go with the ruby Observable. ARObservers seemed to only allow the usual before/after create/update/save/destroy callbacks and looked much more heavyweight than the tiny Observable module. So we did this: (sorry new example, this time with comments.)

Ruby Observable Mixin

Integration Test

def test_create_comment_creates_event
  myuser = create_logged_in_user
  user = create_user
  FeedEvent.delete_all
  post "comments/create", :comment => {:content => 'test', :comment_owner_id => user.id, :comment_owner_type => 'User'}
  assert_equal(1, FeedEvent.count)
end

Controller Test

def test_create_attaches_observer
  user = new_logged_in_user
	c = new_challenge
	post :create, :comment => {:content => 'test', :comment_owner_id => c.id, :comment_owner_type => 'Challenge'}
	assert_equal(1, assigns(:comment).count_observers)
end

Controller Implementation

def create
	@comment = Comment.new params[:comment].merge(:user_id => logged_in_user_id)
	@comment.add_observer CommentObserver.new
  @comment.save
end

Model Test

(using mocha)

def test_create_notifies_observer
  user = new_user
  observer = stub
  comment = Comment.new :comment_owner => user, :content => 'x'*20, :user => new_user
  observer.expects(:update).with(:create, comment)
  comment.add_observer observer
  comment.save!
end

Model Implementation

require 'observer'

class Comment
  include Observable
  after_create :notify_comment_observer

  ...

  private
  def notify_comment_observer
    changed
    notify_observers :create, self
  end
end

Observer Test

def test_update_creates_create_event
  comment_observer = CommentObserver.new
  comment = new_comment
  comment.receiver = new_user
  CommentCreateEvent.expects(:create!).with(:user => comment.receiver, :source => comment)
  comment_observer.update :create, comment
end

Observer Implementation

class CommentObserver
  def update(event_name, comment)
    CommentCreateEvent.create! :user => comment.receiver, :source => comment unless comment.user == comment.receiver || comment.receiver.blank?
  end
end

Wow. That’s a whole lot of code for a tiny little yellow box with one sentence in it. And it’s not even complete because you have to handle deletion of the comments and some other stuff as well. (no event if user comments on his own objects).

Anyway, for some reason we implemented this for 8 types of events or so: Integration test, attach observer in controller, call changed and notify_observers method in model, create event in observer. It was a real pain because we were implementing almost the same thing again and again. Especially attaching the observer in the controller seemed too much work. We thought about doing some meta ruby magic to be able to do the same as rails does with cache sweepers. Instead of attaching the sweeper to the model directly you simply declare that you want this cache sweeper to be active in this action:

class MyController < ApplicationController
  cache_sweeper :comments_sweeper, :only => :create
end

It didn’t really work out because we couldn’t figure out the right magic to do the right things when the model was loaded or created without letting the model do it, which would make the whole use of observers pointless.

ActiveRecord Observers

Finally after a couple of days we took a closer look at the ARObservers. They don’t require you to add them to a model instance every time you want to use them. All you have to do in order to use them is to configure them in your environment:

config.active_record.observers = :message_observer

You then derive you observer class from ActiveRecord::Observer and from then on get all your before/after create/update/save/destroy events for free, delivered right to your observer method with the corresponding name:

class MessageObserver < ActiveRecord::Observer
  def after_create(message)
    MessageReceivedEvent.create! :user => message.receiver, :source => message
  end
end

Now we still had the problem that we wanted custom events, e.g. an after_read event. After some digging deep down in the rails sources we found out how:

class Message
  def read
    self.mark_as_read!
    <strong>callback :after_read</strong>
  end
end

That’s it. Now we implement an after_read method in the observer and we were done.

Now the testing part.

Any idea how the rails guys got the observers attached to every model instance in the app automagically? Well, they attached the observer to the class, not the instance. Sounds like a brilliant idea doesn’t it? But wait. We said one big reason for us to use the observers was that we didn’t want our unit tests to be concerned with this. And now the observers were attached to the classes and all our tests were carrying them as well. Our first solution was to move the observer configuration from environment.rb to the development.rb/production.rb files but this only solved one problem while creating two others: We now had the observer configuration in multiple places and, more importantly, our integration tests didn’t have the observers as well, hence, were now failing.

We ended up doing this: moved the observer configuration back into environment.rb, removed the observers from the model classes before running the unit tests, attached them back before running the integration tests:

# test_helper.rb


# remove all activerecord observers

Dir.glob File.join(RAILS_ROOT, 'app', 'observers', '*.rb') do |file|
  File.basename(file, '_observer.rb').camelize.constantize.delete_observers
end

(We have a separate directory for our obserers in app/obserers)

For our integration tests we created a new file integration_test_helper.rb which we now require instead of the test_helper.rb:

require "#{File.dirname(__FILE__)}/test_helper"
# add observers in case they have been removed by unit tests

Dir.glob File.join(RAILS_ROOT, 'app', 'observers', '*.rb') do |file|
  clazz = File.basename(file, '_observer.rb').camelize.constantize
  clazz.delete_observers
  observer_clazz = File.basename(file, '.rb').camelize.constantize
  observer = observer_clazz.instance if observer_clazz.respond_to?(:instance)
  clazz.add_observer observer if observer.is_a? ActiveRecord::Observer
end

With this setup we now have working unit and integration tests. No need for controller tests and implementation, model tests and implementation only if we have a custom event (such as after_read). What’s left are integration tests and a unit test and implementation of the observer, which is mostly trivial:

class MessageObserverTest &lt; Test::Unit::TestCase

  def setup
    @observer = MessageObserver.instance
    @message = new_message
  end

  def test_create_creates_message_received_event
    MessageReceivedEvent.expects(:create!).with(:user => @message.receiver, :source => @message)
    @observer.after_create(@message)
  end
end