1
0
Fork 0
mirror of https://github.com/thoughtbot/shoulda-matchers.git synced 2022-11-09 12:01:38 -05:00
Simple one-liner tests for common Rails functionality
Find a file
Elliot Winkler 4440445baf Auto-retry bundle install when Travis runs tests
`bundle install` doesn't always work -- sometimes it runs into errors
making HTTP requests, for whatever reason. This will cause Travis to
fail which is pretty annoying.

* Travis supplies an executable called `travis_retry` which will
  automatically retry the command up to 3 times before really failing.
  Tell Travis to use this when it runs `bundle install` before it
  runs tests.
* In spec_helper, we create a Rails app and use this within the
  test suite. This will also run `bundle install`. Unfortunately we
  can't use `travis_retry` for this as it's a function and is not
  available to us in Ruby-land, so use our own retry logic.
2013-12-03 11:09:01 -05:00
features Fix Cucumber tests to work with Rails 4 2013-08-16 16:17:26 -06:00
gemfiles Bump to version 2.4.0 2013-09-20 16:11:59 -04:00
lib Association matchers now support :source option 2013-12-01 15:12:49 -05:00
spec Auto-retry bundle install when Travis runs tests 2013-12-03 11:09:01 -05:00
.gitignore Unignore Gemfile.lock, per Appraisal instructions. 2012-04-10 22:55:29 -04:00
.travis.yml Auto-retry bundle install when Travis runs tests 2013-12-03 11:09:01 -05:00
Appraisals Fix regression with context-dependent validations in ActiveResource 2013-09-11 14:26:40 -06:00
CONTRIBUTING.md Include step to update NEWS in contribution guidelines 2013-03-27 11:11:19 -04:00
Gemfile Bump shoulda-context to ~>1.1.2 for Rails 4 compat 2013-08-16 15:32:08 -06:00
Gemfile.lock Bump to version 2.4.0 2013-09-20 16:11:59 -04:00
MIT-LICENSE Update copyright year to 2013 2013-02-22 10:46:22 -05:00
NEWS.md Association matchers now support :source option 2013-12-01 15:12:49 -05:00
Rakefile Allow the suite to run sequentially via Appraisal, or one at a time with BUNDLE_GEMFILE set 2013-03-04 16:10:54 -05:00
README.md Association matchers now support :source option 2013-12-01 15:12:49 -05:00
shoulda-matchers.gemspec Fix route_matcher specs in Rails 4 2013-08-16 16:17:26 -06:00

shoulda-matchers Gem Version Build Status

Official Documentation

shoulda-matchers provides Test::Unit- and RSpec-compatible one-liners that test common Rails functionality. These tests would otherwise be much longer, more complex, and error-prone.

Installation

Simply add the following to your Gemfile:

group :test do
  gem 'shoulda-matchers'
end

All of the matchers which are listed below will get automatically included into your test framework so you can use them right away.

Usage

Different matchers apply to different parts of Rails:

ActiveModel Matchers

Jump to: allow_mass_assignment_of, allow_value / disallow_value, ensure_inclusion_of, ensure_exclusion_of, ensure_length_of, have_secure_password, validate_acceptance_of, validate_confirmation_of, validate_numericality_of, validate_presence_of, validate_uniqueness_of

allow_mass_assignment_of

The allow_mass_assignment_of matcher tests usage of Rails 3's attr_accessible and attr_protected macros, asserting that attributes can or cannot be mass-assigned on a record.

class Post < ActiveRecord::Base
  attr_accessible :title
  attr_accessible :published_status, as: :admin
end

class User < ActiveRecord::Base
  attr_protected :encrypted_password
end

# RSpec
describe Post do
  it { should allow_mass_assignment_of(:title) }
  it { should allow_mass_assignment_of(:published_status).as(:admin) }
end

describe User do
  it { should_not allow_mass_assignment_of(:encrypted_password) }
end

# Test::Unit
class PostTest < ActiveSupport::TestCase
  should allow_mass_assignment_of(:title)
  should allow_mass_assignment_of(:published_status).as(:admin)
end

class UserTest < ActiveSupport::TestCase
  should_not allow_mass_assignment_of(:encrypted_password)
end

allow_value / disallow_value

The allow_value matcher tests usage of the validates_format_of validation. It asserts that an attribute can be set to one or more values, succeeding if none of the values cause the record to be invalid.

class UserProfile < ActiveRecord::Base
  validates_format_of :website_url, with: URI.regexp

  validates_format_of :birthday_as_string,
    with: /^(\d+)-(\d+)-(\d+)$/,
    on: :create

  validates_format_of :state,
    with: /^(open|closed)$/,
    message: 'State must be open or closed'
end

# RSpec
describe UserProfile do
  it do
    should allow_value('http://foo.com', 'http://bar.com/baz').
      for(:website_url)
  end

  it do
    should_not allow_value('asdfjkl').for(:website_url)
  end

  it do
    should allow_value(:birthday_as_string).on(:create)
  end

  it do
    should allow_value('open', 'closed').
      for(:state).
      with_message('State must be open or closed')
  end
end

# Test::Unit
class UserProfileTest < ActiveSupport::TestCase
  should allow_value('http://foo.com', 'http://bar.com/baz').
    for(:website_url)

  should_not allow_value('asdfjkl').for(:website_url)

  should allow_value(:birthday_as_string).on(:create)

  should allow_value('open', 'closed').
    for(:state).
    with_message('State must be open or closed')
end

IMPORTANT NOTE! Using should_not with allow_value completely negates the assertion. This means that if multiple values are given to allow_value, the matcher succeeds once it sees the first value that will cause the record to be invalid:

describe User do
  # 'b' and 'c' will not be tested
  it { should_not allow_value('a', 'b', 'c').for(:website_url) }
end

The disallow_value matcher is the exact opposite of allow_value. That is, should_not allow_value can also be written should disallow_value, and should allow_value can also be written should_not disallow_value. Because of this, it carries the same caveat when multiple values are provided:

describe User do
  # 'b' and 'c' will not be tested
  it do
    should disallow_value('a', 'b', 'c').for(:website_url)
  end

  it do
    should_not disallow_value('http://foo.com', 'http://bar.com').
      for(:website_url)
  end
end

ensure_inclusion_of

The ensure_inclusion_of matcher tests usage of the validates_inclusion_of validation, asserting that an attribute can take a set of values and cannot take values outside of this set.

class Issue < ActiveRecord::Base
  validates_inclusion_of :state, in: %w(open resolved unresolved)
  validates_inclusion_of :priority, in: 1..5
  validates_inclusion_of :severity,
    in: %w(low medium high),
    message: 'Severity must be low, medium, or high'
end

# RSpec
describe Issue do
  it do
    should ensure_inclusion_of(:state).in_array(%w(open resolved unresolved))
  end

  it do
    should ensure_inclusion_of(:state).in_range(1..5)
  end

  it do
    should ensure_inclusion_of(:severity).
      in_array(%w(low medium high)).
      with_message('Severity must be low, medium, or high')
  end
end

# Test::Unit
class IssueTest < ActiveSupport::TestCase
  should ensure_inclusion_of(:state).in_array(%w(open resolved unresolved))
  should ensure_inclusion_of(:state).in_range(1..5)
  should ensure_inclusion_of(:severity).
    in_array(%w(low medium high)).
    with_message('Severity must be low, medium, or high')
end

ensure_exclusion_of

The ensure_exclusion_of matcher tests usage of the validates_exclusion_of validation, asserting that an attribute cannot take a set of values.

class Game < ActiveRecord::Base
  validates_exclusion_of :supported_os, in: ['Mac', 'Linux']

  validates_exclusion_of :floors_with_enemies, in: 5..8

  validates_exclusion_of :weapon,
    in: ['pistol', 'paintball gun', 'stick'],
    message: 'You chose a puny weapon'
end

# RSpec
describe Game do
  it do
    should ensure_exclusion_of(:supported_os).in_array(['Mac', 'Linux'])
  end

  it { should ensure_exclusion_of(:floors_with_enemies).in_range(5..8) }

  it do
    should ensure_exclusion_of(:weapon).
      in_array(['pistol', 'paintball gun', 'stick']).
      with_message('You chose a puny weapon')
  end
end

# Test::Unit
class GameTest < ActiveSupport::TestCase
  should ensure_exclusion_of(:supported_os).in_array(['Mac', 'Linux'])

  should ensure_exclusion_of(:floors_with_enemies).in_range(5..8)

  should ensure_exclusion_of(:weapon).
    in_array(['pistol', 'paintball gun', 'stick']).
    with_message('You chose a puny weapon')
end

ensure_length_of

The ensure_length_of matcher tests usage of the validates_length_of matcher.

class User < ActiveRecord::Base
  validates_length_of :bio, minimum: 15
  validates_length_of :favorite_superhero, is: 6
  validates_length_of :status_update, maximum: 140
  validates_length_of :password, in: 5..30

  validates_length_of :api_token,
    in: 10..20,
    message: 'API token must be in between 5 and 30 characters'

  validates_length_of :secret_key, in: 15..100,
    too_short: 'Password must be more than 15 characters',
    too_long: 'Password cannot be more than 100 characters'
end

# RSpec
describe User do
  it { should ensure_length_of(:bio).is_at_least(15) }
  it { should ensure_length_of(:favorite_superhero).is_equal_to(6) }
  it { should ensure_length_of(:status_update).is_at_most(140) }
  it { should ensure_length_of(:password).is_at_least(5).is_at_most(30) }

  it do
    should ensure_length_of(:api_token).
      is_at_least(5).
      is_at_most(30).
      message('Password must be in between 5 and 30 characters')
  end

  it do
    should ensure_length_of(:secret_key).
      is_at_least(5).
      is_at_most(30).
      with_short_message('Password must be more than 5 characters').
      with_long_message('Password cannot be more than 30 characters')
  end
end

# Test::Unit
class UserTest < ActiveSupport::TestCase
  should ensure_length_of(:bio).is_at_least(15)
  should ensure_length_of(:favorite_superhero).is_equal_to(6)
  should ensure_length_of(:status_update).is_at_most(140)
  should ensure_length_of(:password).is_at_least(5).is_at_most(30)

  should ensure_length_of(:api_token).
    is_at_least(5).
    is_at_most(30).
    message('Password must be in between 5 and 30 characters')

  should ensure_length_of(:secret_key).
    is_at_least(5).
    is_at_most(30).
    with_short_message('Password must be more than 5 characters').
    with_long_message('Password cannot be more than 30 characters')
end

have_secure_password

The have_secure_password matcher tests usage of the has_secure_password macro.

class User < ActiveRecord::Base
  has_secure_password
end

# RSpec
describe User do
  it { should have_secure_password }
end

# Test::Unit
class UserTest < ActiveSupport::TestCase
  should have_secure_password
end

validate_acceptance_of

The validate_acceptance_of matcher tests usage of the validates_acceptance_of validation.

class Registration
  include ActiveModel::Model

  validates_acceptance_of :eula
  validates_acceptance_of :terms_of_service,
    message: 'You must accept the terms of service'
end

# RSpec
describe Registration do
  it { should validate_acceptance_of(:eula) }

  it do
    should validate_acceptance_of(:terms_of_service).
      with_message('You must accept the terms of service')
  end
end

# Test::Unit
class RegistrationTest < ActiveSupport::TestCase
  should validate_acceptance_of(:eula)

  should validate_acceptance_of(:terms_of_service).
    with_message('You must accept the terms of service')
end

validate_confirmation_of

The validate_confirmation_of matcher tests usage of the validates_confirmation_of validation.

class User < ActiveRecord::Base
  validates_confirmation_of :email
  validates_confirmation_of :password, message: 'Please re-enter your password'
end

# RSpec
describe User do
  it do
    should validate_confirmation_of(:email)
  end

  it do
    should validate_confirmation_of(:password).
      with_message('Please re-enter your password')
  end
end

# Test::Unit
class UserTest < ActiveSupport::TestCase
  should validate_confirmation_of(:email)

  should validate_confirmation_of(:password).
    with_message('Please re-enter your password')
end

validate_numericality_of

The validate_numericality_of matcher tests usage of the validates_numericality_of validation.

class Person < ActiveRecord::Base
  validates_numericality_of :gpa
  validates_numericality_of :age, only_integer: true
  validates_numericality_of :legal_age, greater_than: 21
  validates_numericality_of :height, greater_than_or_equal_to: 55
  validates_numericality_of :weight, equal_to: 150
  validates_numericality_of :number_of_cars, less_than: 2
  validates_numericality_of :birth_year, less_than_or_equal_to: 1987
  validates_numericality_of :birth_day, odd: true
  validates_numericality_of :birth_month, even: true

  validates_numericality_of :number_of_dependents,
    message: 'Number of dependents must be a number'
end

# RSpec
describe Person do
  it { should validate_numericality_of(:gpa) }
  it { should validate_numericality_of(:age).only_integer }
  it { should validate_numericality_of(:legal_age).is_greater_than(21) }
  it { should validate_numericality_of(:height).is_greater_than_or_equal_to(55) }
  it { should validate_numericality_of(:weight).is_equal_to(150) }
  it { should validate_numericality_of(:number_of_cars).is_less_than(2) }
  it { should validate_numericality_of(:birth_year).is_less_than_or_equal_to(1987) }
  it { should validate_numericality_of(:birth_day).odd }
  it { should validate_numericality_of(:birth_month).even }

  it do
    should validate_numericality_of(:number_of_dependents).
      with_message('Number of dependents must be a number' }
  end
end

# Test::Unit
class PersonTest < ActiveSupport::TestCase
  should validate_numericality_of(:gpa)
  should validate_numericality_of(:age).only_integer
  should validate_numericality_of(:legal_age).is_greater_than(21)
  should validate_numericality_of(:height).is_greater_than_or_equal_to(55)
  should validate_numericality_of(:weight).is_equal_to(150)
  should validate_numericality_of(:number_of_cars).is_less_than(2)
  should validate_numericality_of(:birth_year).is_less_than_or_equal_to(1987)
  should validate_numericality_of(:birth_day).odd
  should validate_numericality_of(:birth_month).even

  should validate_numericality_of(:number_of_dependents).
    with_message('Number of dependents must be a number')
end

validate_presence_of

The validate_presence_of matcher tests usage of the validates_presence_of matcher.

class Robot < ActiveRecord::Base
  validates_presence_of :arms
  validates_presence_of :legs, message: 'Robot has no legs'
end

# RSpec
describe Robot do
  it { should validate_presence_of(:arms) }
  it { should validate_presence_of(:legs).with_message('Robot has no legs') }
end

# Test::Unit
class RobotTest < ActiveSupport::TestCase
  should validate_presence_of(:arms)
  should validate_presence_of(:legs).with_message('Robot has no legs')
end

validate_uniqueness_of

The validate_uniqueness_of matcher tests usage of the validates_uniqueness_of validation.

class Post < ActiveRecord::Base
  validates_uniqueness_of :permalink
  validates_uniqueness_of :slug, scope: :user_id
  validates_uniqueness_of :key, case_insensitive: true
  validates_uniqueness_of :author_id, allow_nil: true

  validates_uniqueness_of :title, message: 'Please choose another title'
end

# RSpec
describe Post do
  it { should validate_uniqueness_of(:permalink) }
  it { should validate_uniqueness_of(:slug).scoped_to(:journal_id) }
  it { should validate_uniqueness_of(:key).case_insensitive }
  it { should validate_uniqueness_of(:author_id).allow_nil }

  it do
    should validate_uniqueness_of(:title).
      with_message('Please choose another title')
  end
end

# Test::Unit
class PostTest < ActiveSupport::TestCase
  should validate_uniqueness_of(:permalink)
  should validate_uniqueness_of(:slug).scoped_to(:journal_id)
  should validate_uniqueness_of(:key).case_insensitive
  should validate_uniqueness_of(:author_id).allow_nil

  should validate_uniqueness_of(:title).
    with_message('Please choose another title')
end

IMPORTANT NOTE! This matcher works differently from other validation matchers. Since the very concept of uniqueness depends on checking against a pre-existing record in the database, this matcher will first use the model you're testing to query for such a record, and if it can't find an existing one, it will create one itself. Sometimes this step fails, especially if you have other validations on the attribute you're testing (or, if you have database-level restrictions on any attributes). In this case, the solution is to create a record before you use validate_uniqueness_of.

For example, if you have this model:

class Post < ActiveRecord::Base
  validates_presence_of :permalink
  validates_uniqueness_of :permalink
end

then you will need to test it like this:

describe Post do
  it do
    Post.create!(title: 'This is the title')
    should validate_uniqueness_of(:permalink)
  end
end

ActiveRecord Matchers

Jump to: accept_nested_attributes_for, belong_to, have_many, have_one, have_and_belong_to_many, have_db_column, have_db_index, have_readonly_attribute, serialize

accept_nested_attributes_for

The accept_nested_attributes_for matcher tests usage of the accepts_nested_attributes_for macro.

class Car < ActiveRecord::Base
  accept_nested_attributes_for :doors
  accept_nested_attributes_for :mirrors, allow_destroy: true
  accept_nested_attributes_for :windows, limit: 3
  accept_nested_attributes_for :engine, update_only: true
end

# RSpec
describe Car do
  it { should accept_nested_attributes_for(:doors) }
  it { should accept_nested_attributes_for(:mirrors).allow_destroy(true) }
  it { should accept_nested_attributes_for(:windows).limit(3) }
  it { should accept_nested_attributes_for(:engine).update_only }
end

# Test::Unit (using Shoulda)
class CarTest < ActiveSupport::TestCase
  should accept_nested_attributes_for(:doors)
  should accept_nested_attributes_for(:mirrors).allow_destroy
  should accept_nested_attributes_for(:windows).limit(3)
  should accept_nested_attributes_for(:engine).update_only
end

belong_to

The belong_to matcher tests your belongs_to associations.

class Person < ActiveRecord::Base
  belongs_to :organization
  belongs_to :family, -> { where(everyone_is_perfect: false) }
  belongs_to :previous_company, -> { order('hired_on desc') }
  belongs_to :ancient_city, class_name: 'City'
  belongs_to :great_country, foreign_key: 'country_id'
  belongs_to :mental_institution, touch: true
  belongs_to :world, dependent: :destroy
end

# RSpec
describe Person do
  it { should belong_to(:organization) }
  it { should belong_to(:family).conditions(everyone_is_perfect: false) }
  it { should belong_to(:previous_company).order('hired_on desc') }
  it { should belong_to(:ancient_city).class_name('City') }
  it { should belong_to(:great_country).with_foreign_key('country_id') }
  it { should belong_to(:mental_institution).touch(true) }
  it { should belong_to(:world).dependent(:destroy) }
end

# Test::Unit
class PersonTest < ActiveSupport::TestCase
  should belong_to(:organization)
  should belong_to(:family).conditions(everyone_is_perfect: false)
  should belong_to(:previous_company).order('hired_on desc')
  should belong_to(:ancient_city).class_name('City')
  should belong_to(:great_country).with_foreign_key('country_id')
  should belong_to(:mental_institution).touch(true)
  should belong_to(:world).dependent(:destroy)
end

have_many

The have_many matcher tests your has_many and has_many :through associations.

class Person < ActiveRecord::Base
  has_many :friends
  has_many :acquaintances, through: :friends
  has_many :job_offers, through: :friends, source: :opportunities
  has_many :coins, -> { where(condition: 'mint') }
  has_many :shirts, -> { order('color') }
  has_many :hopes, class_name: 'Dream'
  has_many :worries, foreign_key: 'worrier_id'
  has_many :distractions, counter_cache: true
  has_many :ideas, validate: false
  has_many :topics_of_interest, touch: true
  has_many :secret_documents, dependent: :destroy
end

# RSpec
describe Person do
  it { should have_many(:friends) }
  it { should have_many(:acquaintances).through(:friends) }
  it { should have_many(:job_offers).through(:friends).source(:opportunities) }
  it { should have_many(:coins).conditions(condition: 'mint') }
  it { should have_many(:shirts).order('color') }
  it { should have_many(:hopes).class_name('Dream') }
  it { should have_many(:worries).with_foreign_key('worrier_id') }
  it { should have_many(:ideas).validate(false) }
  it { should have_many(:distractions).counter_cache(true) }
  it { should have_many(:topics_of_interest).touch(true) }
  it { should have_many(:secret_documents).dependent(:destroy) }
end

# Test::Unit
class PersonTest < ActiveSupport::TestCase
  should have_many(:friends)
  should have_many(:acquaintances).through(:friends)
  should have_many(:job_offers).through(:friends).source(:opportunities)
  should have_many(:coins).conditions(condition: 'mint')
  should have_many(:shirts).order('color')
  should have_many(:hopes).class_name('Dream')
  should have_many(:worries).with_foreign_key('worrier_id')
  should have_many(:ideas).validate(false)
  should have_many(:distractions).counter_cache(true)
  should have_many(:topics_of_interest).touch(true)
  should have_many(:secret_documents).dependent(:destroy)
end

have_one

The have_one matcher tests your has_one and has_one :through associations.

class Person < ActiveRecord::Base
  has_one :partner
  has_one :life, through: :partner
  has_one :car, through: :partner, source: :vehicle
  has_one :pet, -> { where('weight < 80') }
  has_one :focus, -> { order('priority desc') }
  has_one :chance, class_name: 'Opportunity'
  has_one :job, foreign_key: 'worker_id'
  has_one :parking_card, validate: false
  has_one :contract, dependent: :nullify
end

# RSpec
describe Person do
  it { should have_one(:partner) }
  it { should have_one(:life).through(:partner) }
  it { should have_one(:car).through(:partner).source(:vehicle) }
  it { should have_one(:pet).conditions('weight < 80') }
  it { should have_one(:focus).order('priority desc') }
  it { should have_one(:chance).class_name('Opportunity') }
  it { should have_one(:job).with_foreign_key('worker_id') }
  it { should have_one(:parking_card).validate(false) }
  it { should have_one(:contract).dependent(:nullify) }
end

# Test::Unit
class PersonTest < ActiveSupport::TestCase
  should have_one(:partner)
  should have_one(:life).through(:partner)
  should have_one(:pet).conditions('weight < 80')
  should have_one(:focus).order('priority desc')
  should have_one(:chance).class_name('Opportunity')
  should have_one(:job).with_foreign_key('worker_id')
  should have_one(:parking_card).validate(false)
  should have_one(:contract).dependent(:nullify)
end

have_and_belong_to_many

The have_and_belong_to_many matcher tests your has_and_belongs_to_many associations.

class Person < ActiveRecord::Base
  has_and_belongs_to_many :awards
  has_and_belongs_to_many :issues, -> { where(difficulty: 'hard') }
  has_and_belongs_to_many :projects, -> { order('time_spent') }
  has_and_belongs_to_many :places_visited, class_name: 'City'
  has_and_belongs_to_many :interviews, validate: false
end

# RSpec
describe Person do
  it { should have_and_belong_to_many(:awards) }
  it { should have_and_belong_to_many(:issues).conditions(difficulty: 'hard') }
  it { should have_and_belong_to_many(:projects).order('time_spent') }
  it { should have_and_belong_to_many(:places_visited).class_name('City') }
  it { should have_and_belong_to_many(:interviews).validate(false) }
end

# Test::Unit
class PersonTest < ActiveSupport::TestCase
  should have_and_belong_to_many(:awards)
  should have_and_belong_to_many(:issues).conditions(difficulty: 'hard')
  should have_and_belong_to_many(:projects).order('time_spent')
  should have_and_belong_to_many(:places_visited).class_name('City')
  should have_and_belong_to_many(:interviews).validate(false)
end

have_db_column

The have_db_column matcher tests that the table that backs your model has a specific column.

class CreatePhones < ActiveRecord::Migration
  def change
    create_table :phones do |t|
      t.decimal :supported_ios_version
      t.string :model, null: false
      t.decimal :camera_aperture, precision: 1
    end
  end
end

# RSpec
describe Phone do
  it { should have_db_column(:supported_ios_version) }
  it { should have_db_column(:model).with_options(null: false) }

  it do
    should have_db_column(:camera_aperture).
      of_type(:decimal).
      with_options(precision: 1)
  end
end

# Test::Unit
class PhoneTest < ActiveSupport::TestCase
  should have_db_column(:supported_ios_version)
  should have_db_column(:model).with_options(null: false)

  should have_db_column(:camera_aperture).
    of_type(:decimal).
    with_options(precision: 1)
end

have_db_index

The have_db_index matcher tests that the table that backs your model has a index on a specific column.

class CreateBlogs < ActiveRecord::Migration
  def change
    create_table :blogs do |t|
      t.integer :user_id, null: false
      t.string :name, null: false
    end

    add_index :blogs, :user_id
    add_index :blogs, :name, unique: true
  end
end

# RSpec
describe Blog do
  it { should have_db_index(:user_id) }
  it { should have_db_index(:name).unique(true) }
end

# Test::Unit
class BlogTest < ActiveSupport::TestCase
  should have_db_index(:user_id)
  should have_db_index(:name).unique(true)
end

have_readonly_attribute

The have_readonly_attribute matcher tests usage of the attr_readonly macro.

class User < ActiveRecord::Base
  attr_readonly :password
end

# RSpec
describe User do
  it { should have_readonly_attribute(:password) }
end

# Test::Unit
class UserTest < ActiveSupport::TestCase
  should have_readonly_attribute(:password)
end

serialize

The serialize matcher tests usage of the serialize macro.

class ProductOptionsSerializer
  def load(string)
    # ...
  end

  def dump(options)
    # ...
  end
end

class Product < ActiveRecord::Base
  serialize :customizations
  serialize :specifications, ProductSpecsSerializer
  serialize :options, ProductOptionsSerializer.new
end

# RSpec
describe Product do
  it { should serialize(:customizations) }
  it { should serialize(:specifications).as(ProductSpecsSerializer) }
  it { should serialize(:options).as_instance_of(ProductOptionsSerializer) }
end

ActionController Matchers

Jump to: filter_param, redirect_to, render_template, render_with_layout, rescue_from, respond_with, set_session, set_the_flash

filter_param

The filter_param matcher tests parameter filtering configuration.

class MyApplication < Rails::Application
  config.filter_parameters << :secret_key
end

# RSpec
describe ApplicationController do
  it { should filter_param(:secret_key) }
end

# RSpec
class ApplicationControllerTest < ActionController::TestCase
  should filter_param(:secret_key)
end

redirect_to

The redirect_to matcher tests that an action redirects to a certain location. In a test suite using RSpec, it is very similar to rspec-rails's redirect_to matcher. In a test suite using Test::Unit / Shoulda, it provides a more expressive syntax over assert_redirected_to.

class PostsController < ApplicationController
  def show
    redirect_to :index
  end
end

# RSpec
describe PostsController do
  describe 'GET #list' do
    before { get :list }

    it { should redirect_to(posts_path) }
  end
end

# Test::Unit
class PostsControllerTest < ActionController::TestCase
  context 'GET #list' do
    setup { get :list }

    should redirect_to { posts_path }
  end
end

render_template

The render_template matcher tests that an action renders a template. In RSpec, it is very similar to rspec-rails's render_template matcher. In Test::Unit, it provides a more expressive syntax over assert_template.

class PostsController < ApplicationController
  def show
  end
end

# RSpec
describe PostsController do
  describe 'GET #show' do
    before { get :show }

    it { should render_template('show') }
  end
end

# Test::Unit
class PostsControllerTest < ActionController::TestCase
  context 'GET #show' do
    setup { get :show }

    should render_template('show')
  end
end

render_with_layout

The render_with_layout matcher tests that an action is rendered with a certain layout.

class PostsController < ApplicationController
  def show
    render layout: 'posts'
  end
end

# RSpec
describe PostsController do
  describe 'GET #show' do
    before { get :show }

    it { should render_with_layout('posts') }
  end
end

# Test::Unit
class PostsControllerTest < ActionController::TestCase
  context 'GET #show' do
    setup { get :show }

    should render_with_layout('posts')
  end
end

rescue_from

The rescue_from matcher tests usage of the rescue_from macro.

class ApplicationController < ActionController::Base
  rescue_from ActiveRecord::RecordNotFound, with: :handle_not_found

  private

  def handle_not_found
    # ...
  end
end

# RSpec
describe ApplicationController do
  it do
    should rescue_from(ActiveRecord::RecordNotFound).
      with(:handle_not_found)
  end
end

# Test::Unit
class ApplicationControllerTest < ActionController::TestCase
  should rescue_from(ActiveRecord::RecordNotFound).
    with(:handle_not_found)
end

respond_with

The respond_with matcher tests that an action responds with a certain status code.

class PostsController < ApplicationController
  def index
    render status: 403
  end

  def show
    render status: :locked
  end

  def destroy
    render status: 508
  end
end

# RSpec
describe PostsController do
  describe 'GET #index' do
    before { get :index }

    it { should respond_with(403) }
  end

  describe 'GET #show' do
    before { get :show }

    it { should respond_with(:locked) }
  end

  describe 'DELETE #destroy' do
    before { delete :destroy }

    it { should respond_with(500..600) }
  end
end

# Test::Unit
class PostsControllerTest < ActionController::TestCase
  context 'GET #index' do
    setup { get :index }

    should respond_with(403)
  end

  context 'GET #show' do
    setup { get :show }

    should respond_with(:locked)
  end

  context 'DELETE #destroy' do
    setup { delete :destroy }

    should respond_with(500..600)
  end
end

route

The route matcher tests that a route resolves to a controller, action, and params; and that the controller, action, and params generates the same route. For an RSpec suite, this is like using a combination of route_to and be_routable. For a Test::Unit suite, it provides a more expressive syntax over assert_routing.

My::Application.routes.draw do
  get '/posts/1' => 'posts#show'
end

# RSpec
describe 'Routing' do
  it { should route(:get, '/posts/1').to(controller: 'posts', action: 'show', id: 1) }

  # a shorter way to say this
  it { should route(:get, '/posts/1').to('posts#show', id: 1) }
end

# Test::Unit
class RoutesTest < ActionController::IntegrationTest
  should route(:get, '/posts/1').to(controller: 'posts', action: 'show', id: 1)

  # a shorter way to say this
  should route(:get, '/posts/1').to('posts#show', id: 1)
end

set_session

The set_session matcher asserts that a key in the session hash has been set to a certain value.

class PostsController < ApplicationController
  def show
    session[:foo] = 'bar'
  end
end

# RSpec
describe PostsController do
  describe 'GET #show' do
    before { get :show }

    it { should set_session(:foo).to('bar') }
    it { should_not set_session(:baz) }
  end
end

# Test::Unit
class PostsControllerTest < ActionController::TestCase
  context 'GET #show' do
    setup { get :show }

    should set_session(:foo).to('bar')
    should_not set_session(:baz)
  end
end

set_the_flash

The set_the_flash matcher asserts that a key in the flash hash is set to a certain value.

class PostsController < ApplicationController
  def index
    flash[:foo] = 'A candy bar'
  end

  def show
    flash.now[:foo] = 'bar'
  end

  def destroy
  end
end

# RSpec
describe PostsController do
  describe 'GET #index' do
    before { get :index }

    it { should set_the_flash.to('bar') }
    it { should set_the_flash.to(/bar/) }
    it { should set_the_flash[:foo].to('bar') }
    it { should_not set_the_flash[:baz] }
  end

  describe 'GET #show' do
    before { get :show }

    it { should set_the_flash.now }
    it { should set_the_flash[:foo].now }
    it { should set_the_flash[:foo].to('bar').now }
  end

  describe 'DELETE #destroy' do
    before { delete :destroy }

    it { should_not set_the_flash }
  end
end

# Test::Unit
class PostsControllerTest < ActionController::TestCase
  context 'GET #index' do
    setup { get :index }

    should set_the_flash.to('bar')
    should set_the_flash.to(/bar/)
    should set_the_flash[:foo].to('bar')
    should_not set_the_flash[:baz]
  end

  context 'GET #show' do
    setup { get :show }

    should set_the_flash.now
    should set_the_flash[:foo].now
    should set_the_flash[:foo].to('bar').now
  end

  context 'DELETE #destroy' do
    setup { delete :destroy }

    should_not set_the_flash
  end
end

Versioning

shoulda-matchers follows Semantic Versioning 2.0 as defined at http://semver.org.

Credits

shoulda-matchers is maintained and funded by thoughtbot. Thank you to all the contributors.

License

shoulda-matchers is copyright © 2006-2013 thoughtbot, inc. It is free software, and may be redistributed under the terms specified in the MIT-LICENSE file.