Rails observers provide a great way to organize your code. If you follow the skinny-controller-fat-model practice in Rails then you may have already crossed the line from comfortably-plump models into massively-obese models. In fact it’s a common misconception that your “models” need to map directly to database tables and it’s quite easy to get carried away adding methods, associations, scopes, validations and callbacks to them. Do not fear - one of the benefits of writing Agile Object-Oriented Ruby is that you can easily simplify this mess with a little refactoring. I’m only going to touch on refactoring callbacks today, but a lot of what I cover here can be implemented for other methods as well.
Let’s consider the following seemingly innocent code. Assume for the time being that in production our emails are actually delivered in the background.
Does this callback really belong on the
User class? In some ways it does, it’s
definitely entirely dependent on the user’s state and does a pretty good job of
delegating to the
GreetingMailer instead of trying to handle the mail
implementation itself. But it does kind of break the “each object should only
deal with one or a few things” OOP pattern. On a more academic level this code
doesn’t really have much to do with the user domain model. It’s only really
responsible for sending a welcome email when something is created.
Let’s rid the
User model of this coupling by refactoring the code into an
WelcomeEmailObserver will have a single responsibility, namely
to “send the appropriate welcome email once something is created”.
Looks good. A couple of things to notice. First, the
User model is simpler.
It is no longer coupled to the
GreetingMailer. In fact, removing or modifying
GreetingMailer does not require us to touch the
User model. A
side-effect of this is also that the
WelcomeEmailObserver is reusable. It is
no longer strictly coupled to the
User. Secondly, we aren’t messing with
The Law of Demeter, the
WelcomeEmailObserver is only referencing its own parameters and first level
attributes of those parameters.
Now that we’ve moved things around, let’s take a look at our tests. First we’ll ensure that they pass and that we didn’t break anything.
Once we’ve verified that the code works, we’ll move the tests from our
user_spec into our
welcome_email_observer_spec. After all we’re no longer
really testing the
That’s a great first step, we’ve definitely simplified our test and are no
longer coupled to the
While it’s not immediately obvious, our first implementation actually strongly
coupled all of our tests that create a
User to the
GreetingMailer. Any time
we created a user we inadvertently also delivered a welcome email. Aside from
creating a dependency between our tests and the success of
methods we’ve also incurred a great performance penalty, namely the code related
to constructing and rendering the email.
While the rendering part may not apply to all of our tests it’s easy to deduce that almost any callback will impose a similar and undesirable overhead. This simple performance penalty is really the main reason that so many Rails test suites take so long to run.
Let’s take a look at a solution to this overhead and how we can simplify our test.
First, we’ll install the no_peeping_toms gem. This gem allows us to turn off ActiveRecord observers on a case-by-case basis. I prefer to implement it by adding the following to my spec helper file.
If you already have observers that are being tested, this change will most likely causes a lot of your tests to fail until you enable the related observers on individual tests. But alone this change can result in a very noticeable performance boost.
The one thing we might be missing from this refactoring is a good integration
test. While it can be argued that testing observer integration is really just
testing Rails, we can make an argument that an integration test is actually
simply testing that our observer is in fact at least observing the
In other words that our observer contains the line
observer :user. So, if we
are compelled to do so, we can also add an integration test to ensure this.
Hopefully this post has enlightened or convinced you of the use of observers and given you some insight into testing them and improving the performance and reliability of the remainder of your tests. I would like to continue writing some more posts about refactoring and testing improvements, so if you have any suggestions please email me or leave a comment.