ryanwhocodes Feb 7, 2018 · 4 min read

Ruby on Rails Callbacks - DRY out your models using Concerns

Learn to write Ruby on Rails Concerns to re-use code in your Models, for example with ActiveRecord callbacks.

When writing an extension for a Ruby on Rails model, an ActiveSupport Concern could be a good choice. They work like mixin modules that can be included in a class, but have some extra benefits for Rails models, such as the ability to add Callbacks.

This post will show you how to DRY out your code so that your Concern not only adds custom methods to records, but also add Callbacks to trigger on create, update, and delete. It will also cover how to run your tests in a way that manages these Callbacks.

Contents

ActiveRecord Callbacks

Callbacks are methods that are triggered when certain events happen in the lifecycle of an ActiveRecord model. A list of ActiveRecord callbacks can be seen on the Rails Guides. To give you an idea of some of the callbacks available:

  • before/after validations
  • before/after save/update/destroy
  • around save/create/update/destroy
  • after_commit/after_rollback (which are linked to database entries / rollbacks)
  • after_initialize (when new is called on a record)
  • after_find (whenever ActiveRecord loads a record from the database)
  • after_touch (when a record is touched)

You can add your own code to be run when these Callbacks are triggered. One way to do this is by using an ActiveSupport::Concern, which can then be easily shared across multiple models.

More on Callbacks can be read about in Rails Guides: Active Record Callbacks.

Using a Callback in a Concern

You can define a Concern using a module that is extended by an ActiveSupport::Concern. Then add the methods and other code you wish to include.

module Publishable
  extend ActiveSupport::Concern

  def email
    UserMailer.weekly_summary(self).deliver_now
  end
end

To add your Concern to a model, you include as you would a module. Then you can tell your model’s callbacks to run the methods from the Concern when they are triggered.

class User < ApplicationRecord
  include Publishable

  after_save -> { email }
end

This means you can create records and call the Concern’s email method on it.

User.new.email

The callback after_save is triggered on create and update, so the email method will be called for the following methods:

  • User.create
  • User.new.save
  • User.update

Extracting the Callback into the Concern

You can also define in your Concern which methods to call for specific callbacks when it is included in a model. This is achieved by using the included method within your Concern.

module Publishable
  extend ActiveSupport::Concern

  included do
    **after_create -> { email }**
  end

  def email
    UserMailer.weekly_summary(self).deliver_now
  end
end

Now your models are kept skinny and all the logic and callback definitions are kept in the Concern.

class User < ApplicationRecord
  include Publishable
end

Testing Callbacks with RSpec

It is important to bear in mind testing when creating Callbacks, because they will be run during your test suite unless you take certain steps to manage them.

For example, you might have a ActiveRecord callback such as after_save :email that uses the Rails ActionMailer to send an email confirmation once a user has been saved.

    class User < ApplicationRecord

      after_save -> { email }

      def email
        UserMailer.confirmation(self).deliver_now
      end
    end

This would post an email every time a record is saved in your tests.

In order to stop this, you need to stub this method call. Depending on your codebase, you either stub individual files or the entire test suite.

In an individual file you would include the following at the start of your RSpec test:

RSpec.describe User do
  subject { FactoryBot.create(:user) }

  before do
    allow(subject).to receive(:email)
  end

  # ... your tests
end

Or for your entire test suite, amend the spec_helper.rb as follows:

RSpec.configure do |config|
  config.before(:each) do
    **allow_any_instance_of(User).to receive(:email)**
  end
end

Warning: this may slow down your test suite!

But then when you do want to test the callback, you need to create the record, set the expectation on it, then call a method that will trigger the callback.

RSpec.describe User do
  describe ‘#create' do
    it ‘calls the Callback #email' do
      record = User.new(params)
      expect(record).to receive(:email)
      record.save
    end
  end
end

If you want to test the full functionality and remove a stub, you use the RSpec method and_call_original, for example:

allow(record).to receive(:email).and_call_original

Find out more

Concerns are a great way to extract logic out of Ruby on Rails models and controllers in a reusable way.

If you are interested in trying out other ways of managing your Ruby on Rails code, then a popular approach is to use Service Objects, which perform a specific task in your app.