Yisroel_dovid_lesches
David Lesches

An Awesome Rails Validations Trick

So here's an awesome little Rails validation trick.

You are obviously familiar with the standard Rails validation usage.

class Post < ActiveRecord::Base

  validates :name, :body, presence: true

end

I'm sure you also know that validations can take an on option to specify that they should run only on create or only on update.

class Post < ActiveRecord::Base

  validates :name, :body, presence: true, on: :create

end

But here's the cool thing: you can actually specify a custom-context for on:

class Post < ActiveRecord::Base

  validates :name, :body, presence: true, on: :foo

end

And those validations only run when triggered explicitly: @post.valid?(:foo)

So Why Is This Cool?

So why is this cool? Because it will give you speedy tests on slow validations, such as a validation that depends on a 3rd-party API.

A classic example of this is a Card model for user's credit cards. In this hypothetical app, we are using Authorize.net CIM for card processing. When a user adds a card, a custom validation sends it off to Authorize.net to be validated and stored there. Within our own database, we only commit the last four letters of the card in order to stay (roughly) PCI-compliant.

class Card < ActiveRecord::Base

  # (this in obviously bare-bones code)

  # Associations
  belongs_to :user

  # Validations
  validates :type, :number, :month, :year, :cvv, presence: true
  validate :validate_credit_card

  # Callbacks
  before_create :trim_number

  # Methods
  def validate_credit_card
    credit_card.validate
    credit_card.errors.each { |key, array| array.each { |value| errors.add(key.to_sym, value) } }
  end

  def trim_number
    self.number = number[-4..-1]
  end


  private

  def credit_card
    @credit_card ||= ActiveMerchant::Billing::CreditCard.new(
      :number             => number,
      :month              => month,
      :year               => year,
      :verification_value => cvv,
      :brand              => brand
    )
  end

end

This works great... until we realize that our tests are crawling. Every test that needs to create a card resource is triggering the API call to Authorize.net.

The typical way out of this problem is to either stub the validate_credit_card repeatedly in our tests, or to create a separate service object to handle the Authorize.net API card creation.

Using the trick above though, we can solve this easily.

First, we add a custom context to the validation.

class Card < ActiveRecord::Base

  # ... other code omitted ...

  validate :validate_credit_card, on: :card_check

  # ... other code omitted ...

end

In our controller, we run the validation explicitly:

def create
  @card = current_user.cards.new(card_params)
  if @card.valid?(:card_check) and @card.save
    flash[:success] = 'Card created.'
    redirect_to @card.user
  else
    flash[:error] = 'Error occurred, see below.'
    render :new
  end   
end

Our test suite, on the other hand, isn't triggering that Authorize.net API call on every card creation, because @card.valid?(:card_check) is not being run.

Kudos

Kudos to @dhh who used this trick in one of his recent code gists.

Enjoyed this article? Let's Grab Coffee! We grab coffee with entrepreneurs all the time.
We'd love to meet you.

GET A QUOTE

Don't be shy. We'd love to hear from you and see how we can help.
 
Coffee

Mmmmm, coffee.

We're fanboys of coffee meetings. Let's meet and chat about anything and everything.
Financier Patisserie
Blue Spoon Coffee
Mulberry & Vine
Starbucks Coffee
Kaffe 1668
Aroma Espresso
Cancel