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 = FactoryBot.create(:user,
      username: 'johnsmith',
      account_id: 1
    )
    user = FactoryBot.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 = FactoryBot.create(:user,
      username: 'johnsmith',
      account_id: 1
    )
    user = FactoryBot.build(:user,
      username: 'JohnSmith',
      account_id: 1
    )
    expect(user).to be_valid
  end
end

With Shoulda Matchers

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

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

Save Time

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

Write icon

Write More Tests

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

Check icon

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 { FactoryBot.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 { FactoryBot.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_value
  • have_secure_password
  • validate_absence_of
  • validate_acceptance_of
  • 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_implicit_order_column
  • have_many
  • have_many_attached
  • have_one
  • have_one_attached
  • have_readonly_attribute
  • have_rich_text
  • serialize
  • validate_uniqueness_of
  • ActionController

  • filter_param
  • permit
  • redirect_to
  • render_template
  • render_with_layout
  • rescue_from
  • respond_with
  • route
  • set_session
  • set_flash
  • use_after_action
  • use_around_action
  • use_before_action
  • Independent Matchers

  • delegate_method