Form object validations in Rails 4

Written by Yuva on March 22, 2014; tagged under ,

Of late, at Codemancers, we are using form objects to decouple forms in views. This also helps in cleaning up how the data filled by end user is consumed and persisted in the backend. So, far the results have been good.

What are form objects

This blog post assumes that you are already familiar with form objects. Railscasts has a nice screencast about form objects. Do check it out if you haven’t already.

Use case

Let’s say that there is an organization and it has several employees. We’re tasked to build a Rails app that provides an interface where an admin can select one or more employees and send them emails. A typical interface implementation might look like this:

Employee email form

After selecting employees, filling-in the subject and body, and upon clicking “Send”, the backend should send emails to the selected employees. This is done by passing the array of the ids of employees, the subject and body to the backend. The POST parameters for that request look like this:

{
  "utf8"=>"✓",
  "email_form"=>{"employee_ids"=>[""], "subject"=>"", "body"=>""},
  "commit"=>"Send emails to employees"
}

Mass mailer form

We will create a EmployeesMassMailerForm form to encapsulate the validations and performing the actual action of sending email. This form should accept the params sent by the form, perform validations like checking whether all the employee ids belong to organization etc., and then send the emails.

class Organization < ActiveRecord::Base
  def get_employees(ids)
    employees.where(id: ids)
  end
end

class EmployeeMassMailerForm
  include ActiveModel::Model

  attr_accessor :organization, :employee_ids, :subject, :body

  validates :organization, :employee_ids, :subject, :body, presence: true
  validate  :employee_ids_should_belong_to_organization

  def perform
    return false unless valid?

    @employees = organization.get_employees(xemployee_ids)
    @employees.each { |e| schedule_email_for(e) }
    true
  end

  private

  def employee_ids_should_belong_to_organization
    if organization.get_employees(employee_ids).length != employee_ids.length
      errors.add(:employee_ids, :invalid)
    end
  end

  def schedule_email_for(e)
    Mailer.send_email(e, subject, body)
  end
end

With Rails 4, ActiveModel ships with Model module which helps in assigning attributes, just like how you can do with ActiveRecord class, along with helpers for validations. It is no longer necessary to use other libraries for form objects. Just include ActiveModel in a PORO class and you are good to go.

Testing using rspec and shoulda

All the form objects can be broken down into 2 main sections:

  1. Validations
  2. Performing actions
Testing validations

Adding validations on forms and models is pretty straight forward. Except for database-related validations like uniqueness, all the ActiveRecord validations can be used on form objects. These validations also make it easy to display validation errors in the view.

At Codemancers, we mostly use rspec and shoulda for testing. Validations on forms can be tested like this:

describe EmployeeMassMailerForm do
  describe 'Validations' do
    it { should validate_presence_of(:organization) }
    it { should validate_presence_of(:employee_ids) }
    it { should validate_presence_of(:subject)      }
    it { should validate_presence_of(:body)         }

    context 'when employee ids belong to organization' do
      it 'validates form successfully' do
        employee_ids = [1, 2]
        organization = mock_model(Organization, get_employees: employees_ids)

        form = described_class.new(organization: organization, subject: 'Test',
                                   employee_ids: employee_ids, body: 'Test')
        expect(form).to be_valid
      end
    end

    context 'when one or more employee ids donot belong organization' do
      it 'fails to validate the form' do
        organization = mock_model(Organization, get_employees: [])

        form = described_class.new(organization: organization, subject: 'Test',
                                   employee_ids: [1, 2, 3], body: 'Test')
        expect(form).to be_invalid
      end
    end
  end
end

You can notice here that while validating employee ids, we use stubs and mock models so that tests never hit database. Testing a form that has validations is a bit hard, because one has to heavily stub and mock models until form becomes valid. But testing an invalid form is easy and sometimes easy to maintain. Notice that we do not care what get_employees returns and that we hard coded it with an empty array whose length is 0. Always try to put as many validations as possible on form object, so that very less exceptions are raised while performing actions.

Testing actions performed by form

Once all the validations pass, the form object will go ahead and perform the action it is supposed to do. It can be anything from sending emails to persisting objects to database. Lets see how we can test the action perform from above form.

describe EmployeeMassMailerForm do
  describe '#perform' do
    let(:organization) do
      employees = [stub(email: 'a@b.com'), stub(email: 'b@c.com')]
      mock_model(Organization, get_employees: employees)
    end

    let(:form) do
      described_class.new(organization: organization, subject: 'Test',
                          employee_ids: [1, 2], body: 'Test')
    end

    before(:each) do
      described_class.any_instance.should_receive(:valid?).and_return(true)
      InvitesMailer.deliveries.clear
    end

    it 'sends emails to all employees' do
      form.perform
      expect(InvitesMailer.deliveries.length).to eq 2
    end

    it 'returns true' do
      expect(form.perform).to be_true
    end
  end
end

The trick here is to hard-code valid? to be true in before block. Since we have already tested validations, we can hard code the return value of valid? to be true. This saves a bunch of db calls and mocks.

I hope you enjoyed this article and if you want to keep updated with latest stuff we are building or blogging about, follow us on twitter @codemancershq.

All you need to know about writing a mountable Rails Gem with rspec

Written by Yuva on May 30, 2013; tagged under , , ,

This article briefly talks about how to write a mountable rails gem and what are the practices that I followed

First things first

Rails generates nice boilerplate for your gem which is sometimes hard to get it right in the first go. Use this simple command and you are good to go

$ rails plugin new <gem-name> --mountable --skip-test-unit --dummy-path=spec/dummy
  1. The --mountable option helps in creating a mountable gem.
  2. The --skip-test-unit skips generation of test templates in test folder
  3. The --dummy-path specifies where the dummy test application {which we will be using for testing} should be generated. It is usually created in test folder but you want that to be created in spec folder so that you can use rspec

Importance of dummy app

Some of the gems that you develop which depend on rails need to run specs in rails environment. Since gem doesn’t have a rails env, it uses dummy app’s rails env to run such tests. This is useful when you write specs for controllers, helpers etc.

When you set up rspec by adding rspec-rails to your Gemfile, you need to add these lines at the top of spec_helper.rb so that rspec loads rails env of dummy app.

ENV["RAILS_ENV"] ||= 'test'
require File.expand_path("../dummy/config/environment", __FILE__)

General flow of development

  1. Never checkin Gemfile.lock in your git repository {or whatever SCM that you are using}
  2. First, add all the dependent gems that your gem needs in Gemfile and develop your gem.
  3. Once you have finalized your dependent gems, move those gem declarations to gemspec file. Add all dependent gems with add_dependency method and add all development dependent gems with add_development_dependency method

    Gem::Specification.new do |s|
      # your information
    
      s.add_dependency "strong_parameters", "~> 0.2.1"
    
      s.add_development_dependency "factory_girl_rails"
    end
  4. Its important to note that you should require your dependent gems explicitly in the root file of your gem. Say if your gem is named my_cool_gem, then you should have my_cool_gem.rb created inside lib folder. If your gem is dependent on strong_parameters, then you need to add these lines:

    require "my_cool_gem/engine"
    require "strong_parameters"
    
    module MyCoolGem
    end

Poor man's rspec mode for emacs

Written by Hemant on March 27, 2013; tagged under , ,

For whatever reason rspec-mode didn’t work for me. I couldn’t either get it to use current version of Ruby or it used to crap out on Bundler detection.

Annoyed, I decided to write a very simple rspec-mode that works almost always. More importantly it runs rspec from root of current project directory using your current shell and hence you don’t need to configure RVM or rbenv from Emacs. Whatever version of Ruby your shell has it will run using that. As a bonus it runs rspec in a comint buffer that means if you are using pry or a debugger you can interact with it from Emacs itself.

(require 'compile)
;; Find root directory by searching for Gemfile
(defun* get-closest-gemfile-root (&optional (file "Gemfile"))
  (let ((root (expand-file-name "/")))
    (loop 
     for d = default-directory then (expand-file-name ".." d)
     if (file-exists-p (expand-file-name file d))
     return d
     if (equal d root)
     return nil)))

(defun rspec-compile-file ()
  (interactive)
  (compile (format "cd %s;bundle exec rspec %s"
                   (get-closest-gemfile-root)
                   (file-relative-name (buffer-file-name) (get-closest-gemfile-root))
                   ) t))

(defun rspec-compile-on-line ()
  (interactive)
  (compile (format "cd %s;bundle exec rspec %s -l %s"
                   (get-closest-gemfile-root)
                   (file-relative-name (buffer-file-name) (get-closest-gemfile-root))
                   (line-number-at-pos)
                   ) t))

(add-hook 'enh-ruby-mode-hook
          (lambda ()
            (local-set-key (kbd "C-c l") 'rspec-compile-on-line)
            (local-set-key (kbd "C-c k") 'rspec-compile-file)
            ))

An astute reader will notice that I am not using default ruby-mode. You can change the hook to ruby-mode-hook if you are using default ruby-mode. So last few lines will become:

(add-hook 'ruby-mode-hook
          (lambda ()
            (local-set-key (kbd "C-c l") 'rspec-compile-on-line)
            (local-set-key (kbd "C-c k") 'rspec-compile-file)
            ))