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


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


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


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 CIM for card processing. When a user adds a card, a custom validation sends it off to 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.errors.each { |key, array| array.each { |value| errors.add(key.to_sym, value) } }

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


  def credit_card
    @credit_card ||=
      :number             => number,
      :month              => month,
      :year               => year,
      :verification_value => cvv,
      :brand              => brand


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

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 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 ...


In our controller, we run the validation explicitly:

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

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


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