Rails Callbacks and APIs
22 Feb 2014How to include API calls in Rails callback hooks without blowing up your tests
While integrating an Instagram media consumer into our app, I ran into a unique use case- I needed to call the Instagram API during model creation/modification, but including after_create
and after_save
callback hooks in the model would make my RSpec tests fail. This was because if I used FactoryGirl.create
to create that model, it would try to make an unnecessary API call.
Here is the original model:
As you can see, it includes an after_create
callback to ping the Instagram API to get the user’s instagram_id
and full_name
. It also has an after_save
callback to ping the Instagram API to follow or unfollow the user on our configured Instagram account.
This resulted in a complicated model test where I had to instantiate the InstagramAccount
object, then stub the two callbacks out, before actually saving it to the test database. Being in Rails 2 made it extra ugly because I couldn’t use the #any_instance
RSpec helper.
The controller spec was even more convoluted, because I couldn’t actually instantiate an object and stub out the instance methods, since the controller handles all of that. So I had to monkey-patch the model, and also un-monkey-patch it so it didn’t affect subsequent tests.
This clearly was a code smell, so after some research, I tried re-factoring the callbacks into an Observer class. This resulted in the after_create
and after_save
hooks being removed from the InstagramAccount model, and moved into InstagramAccountObserver.
Observers are placed in the app/observers/
directory, with a naming convention of #{model_name}Observer
in camel case.
The last step to getting the observer correctly hooked up to observe a model is to add it into your config/environment.rb
file.
Now, for the tests, I added the no-peeping-toms gem by Pat Maddox, which essentially allows you to turn off all observers during RSpec test runs. After adding the gem, it is as simple as adding ActiveRecord::Observer.disable_observers
into your spec_helper.rb
file. This then allowed me to remove the before(:all)
and after(:all)
hooks in my controller test.
In the InstagramAccount model spec, I wanted to test one callback, but didn’t want the other to run. So I enabled observers for that particular test and then stubbed out the one I didn’t need:
After all this, I started reading up on how, in general, callbacks may be code smells in general - even those in Observers:
- http://adequate.io/culling-the-activerecord-lifecycle
I’m not sure if I could implement the collaborating object per that blog post, but I’m going to continue to mull over it.