Simple One-Liner Tests for Rails

Without Shoulda Matchers

describe User do
  it 'is not valid if its username is the same as another user within the same account' do
    _existing_user = FactoryGirl.create(:user,
      username: 'johnsmith',
      account_id: 1
    )
    user = FactoryGirl.build(:user,
      username: 'johnsmith',
      account_id: 1
    )
    expect(user).not_to be_valid
  end

  it 'is valid if its username is the same as another user within the same account, but for different case' do
    _existing_user = FactoryGirl.create(:user,
      username: 'johnsmith',
      account_id: 1
    )
    user = FactoryGirl.build(:user,
      username: 'JohnSmith',
      account_id: 1
    )
    expect(user).to be_valid
  end
end

With Shoulda Matchers

describe User do
  context 'validations' do
    before { FactoryGirl.build(:user) }

    it do
      should validate_uniqueness_of(:username).
        scoped_to(:account_id).
        case_insensitive
    end
  end
end

Save Time

Spend less time writing long, complex, and error-prone tests

Write More Tests

Test thoroughly by using over 30 pre-existing matchers, developed over time

More Readable Results

Get clear, readable, and actionable messages from the tests you run

Extensive matchers for ActiveModel, ActiveRecord, and ActionController

ActiveModel validation matchers

Example: validate_presence_of matcher

Post model
class Robot
  include ActiveModel::Model
  attr_accessor :arms

  validate :arms, presence: true
end
Test using RSpec
describe Robot do
  it { should validate_presence_of(:arms) }
end
Test using Shoulda Context
class RobotTest < ActiveSupport::TestCase
  should validate_presence_of(:arms)
end
Other ActiveModel matchers

Example: have_secure_password matcher

Post model
class User
  include ActiveModel::Model
  include ActiveModel::SecurePassword
  attr_accessor :password

  has_secure_password
end
Test using RSpec
describe User do
  it { should have_secure_password }
end
Test using Shoulda Context
class UserTest < ActiveSupport::TestCase
  should have_secure_password
end
ActiveRecord validation matchers

Example: validate_uniqueness_of matcher

Post model
class Post < ActiveRecord::Base
  validates_uniqueness_of :slug,
    scope: :user_id,
    message: 'duplicate slug within same user_id',
    case_insensitive: true
end
Test using RSpec
describe Post do
  context 'validations' do
    subject { FactoryGirl.build(:post) }

    it do
      should validate_uniqueness_of(:slug).
        scoped_to(:user_id).
        with_message('duplicate slug within same user_id').
        case_insensitive
    end
  end
end
Test using Shoulda Context
class PostTest < ActiveSupport::TestCase
  context 'validations' do
    subject { FactoryGirl.build(:post) }

    should validate_uniqueness_of(:slug).
      scoped_to(:user_id).
      with_message('duplicate slug within same user_id').
      case_insensitive
  end
end
ActiveRecord association matchers

Example: have_many matcher

Post model
class Person < ActiveRecord::Base
  has_many :acquaintances,
    through: :friends,
    class_name: 'Person'
end
Test using RSpec
describe Person do
  it do
    should have_many(:acquaintances).
      through(:friends).
      class_name('Person')
  end
end
Test using Shoulda Context
require 'test_helper'

class PersonTest < ActiveSupport::TestCase
  should have_many(:acquaintances).
    through(:friends).
    class_name('Person')
end
ActionController matchers

Example: rescue_from matcher

Routes
class ApplicationController < ActionController::Base
  rescue_from ActiveRecord::RecordNotFound, with: :render_not_found

  private

  def render_not_found
    # ...
  end
end
Test using RSpec
describe ApplicationController do
  it do
    should rescue_from(ActiveRecord::RecordNotFound).
      with(:render_not_found)
  end
end
Test using Shoulda Context
class ApplicationControllerTest < ActionController::TestCase
  should rescue_from(ActiveRecord::RecordNotFound).
    with(:render_not_found)
end
Independent matchers

Example: delegate_method matcher

app/models/courier.rb
require 'forwardable'

class Courier
  extend Forwardable

  attr_reader :post_office

  def_delegators :post_office, :deliver

  def initialize
    @post_office = PostOffice.new
  end
end
Test using RSpec
describe Courier do
  it { should delegate_method(:deliver).to(:post_office) }
end
Test using Shoulda Context
class CourierTest < Minitest::Test
  should delegate_method(:deliver).to(:post_office)
end

List of Matchers

ActiveModel

  • allow_mass_assignment_of
  • allow_value
  • have_secure_password
  • validate_confirmation_of
  • validate_exclusion_of
  • validate_inclusion_of
  • validate_length_of
  • validate_numericality_of
  • validate_presence_of
  • ActiveRecord

  • accept_nested_attributes_for
  • belong_to
  • define_enum_for
  • have_and_belong_to_many
  • have_db_column
  • have_db_index
  • have_many
  • have_one
  • have_readonly_attribute
  • serialize
  • validate_uniqueness_of
  • ActionController

  • filter_param
  • redirect_to
  • render_template
  • render_with_layout
  • rescue_from
  • respond_with
  • route
  • set_session
  • set_flash
  • use_after_action / use_after_filter
  • use_around_action / use_around_filter
  • use_before_action / use_before_filter
  • Independent Matchers

  • delegate_method