34 KiB
shoulda-matchers
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
shoulda-matchers automatically includes itself into your test framework. It will mix in the appropriate matchers for ActiveRecord, ActiveModel, and ActionController depending on the modules that are available at runtime. For instance, in order to use the ActiveRecord matchers, ActiveRecord must be available beforehand.
If your application is on Rails, everything should "just work", as shoulda-matchers will most likely be declared after Rails in your Gemfile. If your application is on another framework such as Sinatra or Padrino, you may have a different setup, so you will want to ensure that you are requiring shoulda-matchers after the components of Rails you are using.
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_absence_of, validate_acceptance_of, validate_confirmation_of, validate_numericality_of, validate_presence_of, validate_uniqueness_of
Note that all of the examples in this section are based on an ActiveRecord model for simplicity, but these matchers will work just as well using an ActiveModel model.
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 { should allow_value('http://foo.com', 'http://bar.com/baz').for(:website_url) }
it { should_not allow_value('asdfjkl').for(:website_url) }
it { should allow_value(:birthday_as_string).on(:create) }
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
PLEASE 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 { should disallow_value('a', 'b', 'c').for(:website_url) }
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 { should ensure_inclusion_of(:state).in_array(%w(open resolved unresolved))
it { should ensure_inclusion_of(:state).in_range(1..5) }
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 { should ensure_exclusion_of(:supported_os).in_array(['Mac', 'Linux']) }
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 10 and 20 characters'
validates_length_of :secret_key, in: 15..100,
too_short: 'Secret key must be more than 15 characters',
too_long: 'Secret key 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(10).
is_at_most(20).
message('Password must be in between 10 and 20 characters')
end
it do
should ensure_length_of(:secret_key).
is_at_least(15).
is_at_most(100).
with_short_message('Secret key must be more than 15 characters').
with_long_message('Secret key cannot be more than 100 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(15).
is_at_most(20).
message('Password must be in between 15 and 20 characters')
should ensure_length_of(:secret_key).
is_at_least(15).
is_at_most(100).
with_short_message('Secret key must be more than 15 characters').
with_long_message('Secret key cannot be more than 100 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_absence_of
The validate_absence_of
matcher tests the usage of the
validates_absence_of
validation.
class Tank
include ActiveModel::Model
validates_absence_of :arms
validates_absence_of :legs,
message: "Tanks don't have legs."
end
# RSpec
describe Tank do
it { should validate_absence_of(:arms) }
it do
should validate_absence_of(:legs).
with_message("Tanks don't have legs.")
end
end
# Test::Unit
class TankTest < ActiveSupport::TestCase
should validate_absence_of(:arms)
should validate_absence_of(:legs).
with_message("Tanks don't have legs.")
end
validate_acceptance_of
The validate_acceptance_of
matcher tests usage of the
validates_acceptance_of
validation.
class Registration < ActiveRecord::Base
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
PLEASE 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(true) }
end
# Test::Unit (using Shoulda)
class CarTest < ActiveSupport::TestCase
should accept_nested_attributes_for(:doors)
should accept_nested_attributes_for(:mirrors).allow_destroy(true)
should accept_nested_attributes_for(:windows).limit(3)
should accept_nested_attributes_for(:engine).update_only(true)
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(:car).through(:partner).source(:vehicle)
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
# Test::Unit
class ProductTest < ActiveSupport::TestCase
should serialize(:customizations)
should serialize(:specifications).as(ProductSpecsSerializer)
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, route, 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
# Test::Unit
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', controller: 'posts', action: 'index'
get '/posts/:id' => 'posts#show'
end
# RSpec
describe 'Routing' do
it { should route(:get, '/posts').to(controller: 'posts', action: 'index') }
it { should route(:get, '/posts/1').to('posts#show', id: 1) }
end
# Test::Unit
class RoutesTest < ActionController::IntegrationTest
should route(:get, '/posts').to(controller: 'posts', action: 'index')
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.