1
0
Fork 0
mirror of https://github.com/thoughtbot/shoulda-matchers.git synced 2022-11-09 12:01:38 -05:00

Move into the Matchers namespace

This commit is contained in:
Joe Ferris 2010-12-15 17:34:19 -05:00
parent ecb01a209d
commit 56b0a0439e
94 changed files with 2577 additions and 2518 deletions

View file

@ -7,7 +7,7 @@ require 'rspec/core/rake_task'
require 'cucumber/rake/task'
$LOAD_PATH.unshift("lib")
require 'shoulda/version'
require 'shoulda/matchers/version'
Rake::RDocTask.new { |rdoc|
rdoc.rdoc_dir = 'doc'
@ -31,7 +31,7 @@ RSpec::Core::RakeTask.new(:coverage) do |t|
t.pattern = "spec/**/*_spec.rb"
end
eval("$specification = begin; #{IO.read('shoulda.gemspec')}; end")
eval("$specification = begin; #{IO.read('shoulda-matchers.gemspec')}; end")
Rake::GemPackageTask.new $specification do |pkg|
pkg.need_tar = true
pkg.need_zip = true

View file

@ -33,7 +33,7 @@ Feature: integrate with Rails
Scenario: generate a rails application and use matchers in Test::Unit
When I configure the application to use shoulda-context
And I configure the application to use "shoulda" from this project
And I configure the application to use "shoulda-matchers" from this project
And I write to "test/unit/user_test.rb" with:
"""
require 'test_helper'
@ -63,7 +63,7 @@ Feature: integrate with Rails
Scenario: generate a rails application and use matchers in Rspec
When I configure the application to use rspec-rails
And I configure the application to use "shoulda" from this project
And I configure the application to use "shoulda-matchers" from this project
And I run the rspec generator
And I write to "spec/models/user_spec.rb" with:
"""

8
lib/shoulda-matchers.rb Normal file
View file

@ -0,0 +1,8 @@
require 'shoulda/matchers/version'
if defined?(RSpec)
require 'shoulda/matchers/integrations/rspec'
else
require 'shoulda/matchers/integrations/test_unit'
end

View file

@ -1,8 +0,0 @@
require 'shoulda/version'
if defined?(RSpec)
require 'shoulda/integrations/rspec'
else
require 'shoulda/integrations/test_unit'
end

View file

@ -1,36 +0,0 @@
require 'shoulda/action_controller/assign_to_matcher'
require 'shoulda/action_controller/filter_param_matcher'
require 'shoulda/action_controller/set_the_flash_matcher'
require 'shoulda/action_controller/render_with_layout_matcher'
require 'shoulda/action_controller/respond_with_matcher'
require 'shoulda/action_controller/respond_with_content_type_matcher'
require 'shoulda/action_controller/set_session_matcher'
require 'shoulda/action_controller/route_matcher'
require 'shoulda/action_controller/redirect_to_matcher'
require 'shoulda/action_controller/render_template_matcher'
module Shoulda
# By using the matchers you can quickly and easily create concise and
# easy to read test suites.
#
# This code segment:
#
# describe UsersController, "on GET to show with a valid id" do
# before(:each) do
# get :show, :id => User.first.to_param
# end
#
# it { should assign_to(:user) }
# it { should respond_with(:success) }
# it { should render_template(:show) }
# it { should not_set_the_flash) }
#
# it "should do something else really cool" do
# assigns[:user].id.should == 1
# end
# end
#
# Would produce 5 tests for the show action
module ActionController
end
end

View file

@ -1,112 +0,0 @@
module Shoulda # :nodoc:
module ActionController # :nodoc:
# Ensures that the controller assigned to the named instance variable.
#
# Options:
# * <tt>with_kind_of</tt> - The expected class of the instance variable
# being checked.
# * <tt>with</tt> - The value that should be assigned.
#
# Example:
#
# it { should assign_to(:user) }
# it { should_not assign_to(:user) }
# it { should assign_to(:user).with_kind_of(User) }
# it { should assign_to(:user).with(@user) }
def assign_to(variable)
AssignToMatcher.new(variable)
end
class AssignToMatcher # :nodoc:
def initialize(variable)
@variable = variable.to_s
@check_value = false
end
def with_kind_of(expected_class)
@expected_class = expected_class
self
end
def with(expected_value = nil, &block)
@check_value = true
@expected_value = expected_value
@expectation_block = block
self
end
def matches?(controller)
@controller = controller
@expected_value = @context.instance_eval(&@expectation_block) if @expectation_block
assigned_value? && kind_of_expected_class? && equal_to_expected_value?
end
attr_reader :failure_message, :negative_failure_message
def description
description = "assign @#{@variable}"
description << " with a kind of #{@expected_class}" if @expected_class
description
end
def in_context(context)
@context = context
self
end
private
def assigned_value?
if !@controller.instance_variables.include?("@#{@variable}")
@failure_message =
"Expected action to assign a value for @#{@variable}"
false
else
@negative_failure_message =
"Didn't expect action to assign a value for @#{@variable}, " <<
"but it was assigned to #{assigned_value.inspect}"
true
end
end
def kind_of_expected_class?
return true unless @expected_class
if assigned_value.kind_of?(@expected_class)
@negative_failure_message =
"Didn't expect action to assign a kind of #{@expected_class} " <<
"for #{@variable}, but got one anyway"
true
else
@failure_message =
"Expected action to assign a kind of #{@expected_class} " <<
"for #{@variable}, but got #{@variable.inspect} " <<
"(#{@variable.class.name})"
false
end
end
def equal_to_expected_value?
return true unless @check_value
if @expected_value == assigned_value
@negative_failure_message =
"Didn't expect action to assign #{@expected_value.inspect} " <<
"for #{@variable}, but got it anyway"
true
else
@failure_message =
"Expected action to assign #{@expected_value.inspect} " <<
"for #{@variable}, but got #{assigned_value.inspect}"
false
end
end
def assigned_value
@controller.instance_variable_get("@#{@variable}")
end
end
end
end

View file

@ -1,48 +0,0 @@
module Shoulda # :nodoc:
module ActionController # :nodoc:
# Ensures that filter_parameter_logging is set for the specified key.
#
# Example:
#
# it { should filter_param(:password) }
def filter_param(key)
FilterParamMatcher.new(key)
end
class FilterParamMatcher # :nodoc:
def initialize(key)
@key = key.to_s
end
def matches?(controller)
@controller = controller
filters_key?
end
def failure_message
"Expected #{@key} to be filtered; filtered keys: #{filtered_keys.join(', ')}"
end
def negative_failure_message
"Did not expect #{@key} to be filtered"
end
def description
"filter #{@key}"
end
private
def filters_key?
filtered_keys.include?(@key)
end
def filtered_keys
Rails.application.config.filter_parameters.map { |filter| filter.to_s }
end
end
end
end

View file

@ -1,60 +0,0 @@
module Shoulda # :nodoc:
module ActionController # :nodoc:
# Ensures a controller redirected to the given url.
#
# Example:
#
# it { should redirect_to('http://somewhere.com') }
# it { should redirect_to(users_path) }
def redirect_to(url_or_description, &block)
RedirectToMatcher.new(url_or_description, self, &block)
end
class RedirectToMatcher # :nodoc:
def initialize(url_or_description, context, &block)
if block
@url_block = block
@location = @url_or_description
else
@url = url_or_description
@location = @url
end
@context = context
end
def in_context(context)
@context = context
self
end
def matches?(controller)
@controller = controller
redirects_to_url?
end
attr_reader :failure_message, :negative_failure_message
def description
"redirect to #{@location}"
end
private
def redirects_to_url?
@url = @context.instance_eval(&@url_block) if @url_block
begin
@context.send(:assert_redirected_to, @url)
@negative_failure_message = "Didn't expect to redirect to #{@url}"
true
rescue Test::Unit::AssertionFailedError => error
@failure_message = error.message
false
end
end
end
end
end

View file

@ -1,52 +0,0 @@
module Shoulda # :nodoc:
module ActionController # :nodoc:
# Ensures a controller rendered the given template.
#
# Example:
#
# it { should render_template(:show) }
def render_template(template)
RenderTemplateMatcher.new(template, self)
end
class RenderTemplateMatcher # :nodoc:
def initialize(template, context)
@template = template.to_s
@context = context
end
def matches?(controller)
@controller = controller
renders_template?
end
attr_reader :failure_message, :negative_failure_message
def description
"render template #{@template}"
end
def in_context(context)
@context = context
self
end
private
def renders_template?
begin
@context.send(:assert_template, @template)
@negative_failure_message = "Didn't expect to render #{@template}"
true
rescue Test::Unit::AssertionFailedError => error
@failure_message = error.message
false
end
end
end
end
end

View file

@ -1,97 +0,0 @@
module Shoulda # :nodoc:
module ActionController # :nodoc:
# Ensures that the controller rendered with the given layout.
#
# Example:
#
# it { should render_with_layout }
# it { should render_with_layout(:special) }
# it { should_not render_with_layout }
def render_with_layout(expected_layout = nil)
RenderWithLayoutMatcher.new(expected_layout).in_context(self)
end
class RenderWithLayoutMatcher # :nodoc:
def initialize(expected_layout)
@expected_layout = expected_layout.to_s unless expected_layout.nil?
end
# Used to provide access to layouts recorded by
# ActionController::TemplateAssertions in Rails 3
def in_context(context)
@context = context
self
end
def matches?(controller)
@controller = controller
rendered_with_layout? && rendered_with_expected_layout?
end
def failure_message
"Expected #{expectation}, but #{result}"
end
def negative_failure_message
"Did not expect #{expectation}, but #{result}"
end
def description
description = "render with "
if @expected_layout.nil?
description << "a layout"
else
description << "the #{@expected_layout.inspect} layout"
end
description
end
private
def rendered_with_layout?
!rendered_layouts.empty?
end
def rendered_with_expected_layout?
return true if @expected_layout.nil?
rendered_layouts.include?(@expected_layout)
end
def rendered_layouts
if recorded_layouts
recorded_layouts.keys.compact.map { |layout| layout.sub(%r{^layouts/}, '') }
else
layout = @controller.response.layout
if layout.nil?
[]
else
[layout.split('/').last]
end
end
end
def recorded_layouts
if @context
@context.instance_variable_get('@layouts')
end
end
def expectation
"to #{description}"
end
def result
if rendered_with_layout?
"rendered with " <<
rendered_layouts.map { |layout| layout.inspect }.join(", ")
else
"rendered without a layout"
end
end
end
end
end

View file

@ -1,72 +0,0 @@
module Shoulda # :nodoc:
module ActionController # :nodoc:
# Ensures a controller responded with expected 'response' content type.
#
# You can pass an explicit content type such as 'application/rss+xml'
# or its symbolic equivalent :rss
# or a regular expression such as /rss/
#
# Example:
#
# it { should respond_with_content_type(:xml) }
# it { should respond_with_content_type(:csv) }
# it { should respond_with_content_type(:atom) }
# it { should respond_with_content_type(:yaml) }
# it { should respond_with_content_type(:text) }
# it { should respond_with_content_type('application/rss+xml') }
# it { should respond_with_content_type(/json/) }
def respond_with_content_type(content_type)
RespondWithContentTypeMatcher.new(content_type)
end
class RespondWithContentTypeMatcher # :nodoc:
def initialize(content_type)
@content_type = if content_type.is_a?(Symbol)
lookup_by_extension(content_type)
else
content_type
end
end
def description
"respond with content type of #{@content_type}"
end
def matches?(controller)
@controller = controller
if @content_type.is_a?(Regexp)
response_content_type =~ @content_type
else
response_content_type == @content_type
end
end
def failure_message
"Expected #{expectation}"
end
def negative_failure_message
"Did not expect #{expectation}"
end
protected
def response_content_type
@controller.response.content_type.to_s
end
def lookup_by_extension(extension)
Mime::Type.lookup_by_extension(extension.to_s).to_s
end
def expectation
"content type to be #{@content_type}, " <<
"but was #{response_content_type}"
end
end
end
end

View file

@ -1,83 +0,0 @@
module Shoulda # :nodoc:
module ActionController # :nodoc:
# Ensures a controller responded with expected 'response' status code.
#
# You can pass an explicit status number like 200, 301, 404, 500
# or its symbolic equivalent :success, :redirect, :missing, :error.
# See ActionController::StatusCodes for a full list.
#
# Example:
#
# it { should respond_with(:success) }
# it { should respond_with(:redirect) }
# it { should respond_with(:missing) }
# it { should respond_with(:error) }
# it { should respond_with(501) }
def respond_with(status)
RespondWithMatcher.new(status)
end
class RespondWithMatcher # :nodoc:
def initialize(status)
@status = symbol_to_status_code(status)
end
def matches?(controller)
@controller = controller
correct_status_code? || correct_status_code_range?
end
def failure_message
"Expected #{expectation}"
end
def negative_failure_message
"Did not expect #{expectation}"
end
def description
"respond with #{@status}"
end
protected
def correct_status_code?
response_code == @status
end
def correct_status_code_range?
@status.is_a?(Range) &&
@status.include?(response_code)
end
def response_code
@controller.response.response_code
end
def symbol_to_status_code(potential_symbol)
case potential_symbol
when :success then 200
when :redirect then 300..399
when :missing then 404
when :error then 500..599
when Symbol
if defined?(::Rack::Utils::SYMBOL_TO_STATUS_CODE)
::Rack::Utils::SYMBOL_TO_STATUS_CODE[potential_symbol]
else
::ActionController::Base::SYMBOL_TO_STATUS_CODE[potential_symbol]
end
else
potential_symbol
end
end
def expectation
"response to be a #{@status}, but was #{response_code}"
end
end
end
end

View file

@ -1,91 +0,0 @@
module Shoulda # :nodoc:
module ActionController # :nodoc:
# Ensures that requesting +path+ using +method+ routes to +options+.
#
# If you don't specify a controller, it will use the controller from the
# example group.
#
# +to_param+ is called on the +options+ given.
#
# Examples:
#
# it { should route(:get, "/posts").
# to(:controller => :posts, :action => :index) }
# it { should route(:get, "/posts/new").to(:action => :new) }
# it { should route(:post, "/posts").to(:action => :create) }
# it { should route(:get, "/posts/1").to(:action => :show, :id => 1) }
# it { should route(:edit, "/posts/1").to(:action => :show, :id => 1) }
# it { should route(:put, "/posts/1").to(:action => :update, :id => 1) }
# it { should route(:delete, "/posts/1").
# to(:action => :destroy, :id => 1) }
# it { should route(:get, "/users/1/posts/1").
# to(:action => :show, :id => 1, :user_id => 1) }
def route(method, path)
RouteMatcher.new(method, path, self)
end
class RouteMatcher # :nodoc:
def initialize(method, path, context)
@method = method
@path = path
@context = context
end
def to(params)
@params = params
stringify_params!
self
end
def in_context(context)
@context = context
self
end
def matches?(controller)
@controller = controller
guess_controller!
route_recognized?
end
attr_reader :failure_message, :negative_failure_message
def description
"route #{@method.to_s.upcase} #{@path} to/from #{@params.inspect}"
end
private
def guess_controller!
@params[:controller] ||= @controller.controller_path
end
def stringify_params!
@params.each do |key, value|
@params[key] = value.is_a?(Array) ? value.collect {|v| v.to_param } : value.to_param
end
end
def route_recognized?
begin
@context.send(:assert_routing,
{ :method => @method, :path => @path },
@params)
@negative_failure_message = "Didn't expect to #{description}"
true
rescue ::ActionController::RoutingError => error
@failure_message = error.message
false
rescue Test::Unit::AssertionFailedError => error
@failure_message = error.message
false
end
end
end
end
end

View file

@ -1,96 +0,0 @@
module Shoulda # :nodoc:
module ActionController # :nodoc:
# Ensures that a session key was set to the expected value.
#
# Example:
#
# it { should set_session(:message) }
# it { should set_session(:user_id).to(@user.id) }
# it { should_not set_session(:user_id) }
def set_session(key)
SetSessionMatcher.new(key)
end
class SetSessionMatcher # :nodoc:
def initialize(key)
@key = key.to_s
end
def to(value = nil, &block)
@value = value
@value_block = block
self
end
def matches?(controller)
@controller = controller
@value = @context.instance_eval(&@value_block) if @value_block
(assigned_value? && assigned_correct_value?) || cleared_value?
end
def failure_message
"Expected #{expectation}, but #{result}"
end
def negative_failure_message
"Didn't expect #{expectation}, but #{result}"
end
def description
description = "set session variable #{@key.inspect}"
description << " to #{@value.inspect}" if defined?(@value)
description
end
def in_context(context)
@context = context
self
end
private
def assigned_value?
!assigned_value.nil?
end
def cleared_value?
defined?(@value) && @value.nil? && assigned_value.nil?
end
def assigned_correct_value?
return true if @value.nil?
assigned_value == @value
end
def assigned_value
session[@key]
end
def session
if @controller.request.respond_to?(:session)
@controller.request.session.to_hash
else
@controller.response.session.data
end
end
def expectation
expectation = "session variable #{@key} to be set"
expectation << " to #{@value.inspect}" if @value
expectation
end
def result
if session.empty?
"no session variables were set"
else
"the session was #{session.inspect}"
end
end
end
end
end

View file

@ -1,92 +0,0 @@
module Shoulda # :nodoc:
module ActionController # :nodoc:
# Ensures that the flash contains the given value. Can be a String, a
# Regexp, or nil (indicating that the flash should not be set).
#
# Example:
#
# it { should set_the_flash }
# it { should set_the_flash.to("Thank you for placing this order.") }
# it { should set_the_flash.to(/created/i) }
# it { should set_the_flash.to(/logged in/i).now }
# it { should_not set_the_flash }
def set_the_flash
SetTheFlashMatcher.new
end
class SetTheFlashMatcher # :nodoc:
def to(value)
@value = value
self
end
def now
@now = true
self
end
def matches?(controller)
@controller = controller
sets_the_flash? && string_value_matches? && regexp_value_matches?
end
attr_reader :failure_message, :negative_failure_message
def description
description = "set the flash"
description << " to #{@value.inspect}" unless @value.nil?
description
end
def failure_message
"Expected #{expectation}"
end
def negative_failure_message
"Did not expect #{expectation}"
end
private
def sets_the_flash?
!flash.blank?
end
def string_value_matches?
return true unless String === @value
flash.values.any? {|value| value == @value }
end
def regexp_value_matches?
return true unless Regexp === @value
flash.values.any? {|value| value =~ @value }
end
def flash
return @flash if @flash
@flash = @controller.flash.dup
@flash.sweep unless @now
@flash
end
def expectation
expectation = "the flash#{".now" if @now} to be set"
expectation << " to #{@value.inspect}" unless @value.nil?
expectation << ", but #{flash_description}"
expectation
end
def flash_description
if flash.blank?
"no flash was set"
else
"was #{flash.inspect}"
end
end
end
end
end

View file

@ -1,20 +0,0 @@
require 'shoulda/action_mailer/have_sent_email'
module Shoulda
# = Matchers for your mailers
#
# This matcher will test that email is sent properly
#
# describe User do
# it { should have_sent_email.with_subject(/is spam$/) }
# it { should have_sent_email.from('do-not-reply@example.com') }
# it { should have_sent_email.with_body(/is spam\./) }
# it { should have_sent_email.to('myself@me.com') }
# it { should have_sent_email.with_subject(/spam/).
# from('do-not-reply@example.com').
# with_body(/spam/).
# to('myself@me.com') }
# end
module ActionMailer
end
end

View file

@ -1,108 +0,0 @@
module Shoulda # :nodoc:
module ActionMailer # :nodoc:
# The right email is sent.
#
# it { should have_sent_email.with_subject(/is spam$/) }
# it { should have_sent_email.from('do-not-reply@example.com') }
# it { should have_sent_email.with_body(/is spam\./) }
# it { should have_sent_email.to('myself@me.com') }
# it { should have_sent_email.with_subject(/spam/).
# from('do-not-reply@example.com').
# with_body(/spam/).
# to('myself@me.com') }
def have_sent_email
HaveSentEmailMatcher.new
end
class HaveSentEmailMatcher # :nodoc:
def initialize
end
def with_subject(email_subject)
@email_subject = email_subject
self
end
def from(sender)
@sender = sender
self
end
def with_body(body)
@body = body
self
end
def to(recipient)
@recipient = recipient
self
end
def matches?(subject)
::ActionMailer::Base.deliveries.each do |mail|
@subject_failed = !regexp_or_string_match(mail.subject, @email_subject) if @email_subject
@body_failed = !regexp_or_string_match(mail.body, @body) if @body
@sender_failed = !regexp_or_string_match_in_array(mail.from, @sender) if @sender
@recipient_failed = !regexp_or_string_match_in_array(mail.to, @recipient) if @recipient
return true unless anything_failed?
end
false
end
def failure_message
"Expected #{expectation}"
end
def negative_failure_message
"Did not expect #{expectation}"
end
def description
"send an email"
end
private
def expectation
expectation = "sent email"
expectation << " with subject #{@email_subject.inspect}" if @subject_failed
expectation << " with body #{@body.inspect}" if @body_failed
expectation << " from #{@sender.inspect}" if @sender_failed
expectation << " to #{@recipient.inspect}" if @recipient_failed
expectation << "\nDeliveries:\n#{inspect_deliveries}"
end
def inspect_deliveries
::ActionMailer::Base.deliveries.map do |delivery|
"#{delivery.subject.inspect} to #{delivery.to.inspect}"
end.join("\n")
end
def anything_failed?
@subject_failed || @body_failed || @sender_failed || @recipient_failed
end
def regexp_or_string_match(a_string, a_regexp_or_string)
case a_regexp_or_string
when Regexp
a_string =~ a_regexp_or_string
when String
a_string == a_regexp_or_string
end
end
def regexp_or_string_match_in_array(an_array, a_regexp_or_string)
case a_regexp_or_string
when Regexp
an_array.any? { |string| string =~ a_regexp_or_string }
when String
an_array.include?(a_regexp_or_string)
end
end
end
end
end

View file

@ -1,40 +0,0 @@
require 'shoulda/active_record/helpers'
require 'shoulda/active_record/validation_matcher'
require 'shoulda/active_record/allow_value_matcher'
require 'shoulda/active_record/ensure_length_of_matcher'
require 'shoulda/active_record/ensure_inclusion_of_matcher'
require 'shoulda/active_record/validate_presence_of_matcher'
require 'shoulda/active_record/validate_format_of_matcher'
require 'shoulda/active_record/validate_uniqueness_of_matcher'
require 'shoulda/active_record/validate_acceptance_of_matcher'
require 'shoulda/active_record/validate_numericality_of_matcher'
require 'shoulda/active_record/association_matcher'
require 'shoulda/active_record/have_db_column_matcher'
require 'shoulda/active_record/have_db_index_matcher'
require 'shoulda/active_record/have_readonly_attribute_matcher'
require 'shoulda/active_record/allow_mass_assignment_of_matcher'
module Shoulda
# = Matchers for your active record models
#
# These matchers will test most of the validations and associations for your
# ActiveRecord models.
#
# describe User do
# it { should validate_presence_of(:name) }
# it { should validate_presence_of(:phone_number) }
# %w(abcd 1234).each do |value|
# it { should_not allow_value(value).for(:phone_number) }
# end
# it { should allow_value("(123) 456-7890").for(:phone_number) }
# it { should_not allow_mass_assignment_of(:password) }
# it { should have_one(:profile) }
# it { should have_many(:dogs) }
# it { should have_many(:messes).through(:dogs) }
# it { should belong_to(:lover) }
# end
#
module ActiveRecord
end
end

View file

@ -1,81 +0,0 @@
module Shoulda # :nodoc:
module ActiveRecord # :nodoc:
# Ensures that the attribute can be set on mass update.
#
# it { should_not allow_mass_assignment_of(:password) }
# it { should allow_mass_assignment_of(:first_name) }
#
def allow_mass_assignment_of(value)
AllowMassAssignmentOfMatcher.new(value)
end
class AllowMassAssignmentOfMatcher # :nodoc:
def initialize(attribute)
@attribute = attribute.to_s
end
def matches?(subject)
@subject = subject
if attr_mass_assignable?
if whitelisting?
@negative_failure_message = "#{@attribute} was made accessible"
else
if protected_attributes.empty?
@negative_failure_message = "no attributes were protected"
else
@negative_failure_message = "#{class_name} is protecting " <<
"#{protected_attributes.to_a.to_sentence}, " <<
"but not #{@attribute}."
end
end
true
else
if whitelisting?
@failure_message =
"Expected #{@attribute} to be accessible"
else
@failure_message =
"Did not expect #{@attribute} to be protected"
end
false
end
end
attr_reader :failure_message, :negative_failure_message
def description
"allow mass assignment of #{@attribute}"
end
private
def protected_attributes
@protected_attributes ||= (@subject.class.protected_attributes || [])
end
def accessible_attributes
@accessible_attributes ||= (@subject.class.accessible_attributes || [])
end
def whitelisting?
!accessible_attributes.empty?
end
def attr_mass_assignable?
if whitelisting?
accessible_attributes.include?(@attribute)
else
!protected_attributes.include?(@attribute)
end
end
def class_name
@subject.class.name
end
end
end
end

View file

@ -1,108 +0,0 @@
module Shoulda # :nodoc:
module ActiveRecord # :nodoc:
# Ensures that the attribute can be set to the given value.
#
# Options:
# * <tt>with_message</tt> - value the test expects to find in
# <tt>errors.on(:attribute)</tt>. Regexp or string. If omitted,
# the test looks for any errors in <tt>errors.on(:attribute)</tt>.
#
# Example:
# it { should_not allow_value('bad').for(:isbn) }
# it { should allow_value("isbn 1 2345 6789 0").for(:isbn) }
#
def allow_value(value)
AllowValueMatcher.new(value)
end
class AllowValueMatcher # :nodoc:
include Helpers
def initialize(value)
@value = value
end
def for(attribute)
@attribute = attribute
self
end
def with_message(message)
@expected_message = message if message
self
end
def matches?(instance)
@instance = instance
if Symbol === @expected_message
@expected_message = default_error_message(@expected_message)
end
@instance.send("#{@attribute}=", @value)
!errors_match?
end
def failure_message
"Did not expect #{expectation}, got error: #{@matched_error}"
end
def negative_failure_message
"Expected #{expectation}, got #{error_description}"
end
def description
"allow #{@attribute} to be set to #{@value.inspect}"
end
private
def errors_match?
@instance.valid?
@errors = errors_for_attribute(@instance, @attribute)
@errors = [@errors] unless @errors.is_a?(Array)
@expected_message ? (errors_match_regexp? || errors_match_string?) : (@errors.compact.any?)
end
def errors_for_attribute(instance, attribute)
if instance.errors.respond_to?(:[])
instance.errors[attribute]
else
instance.errors.on(attribute)
end
end
def errors_match_regexp?
if Regexp === @expected_message
@matched_error = @errors.detect { |e| e =~ @expected_message }
!@matched_error.nil?
else
false
end
end
def errors_match_string?
if @errors.include?(@expected_message)
@matched_error = @expected_message
true
else
false
end
end
def expectation
"errors " <<
(@expected_message ? "to include #{@expected_message.inspect} " : "") <<
"when #{@attribute} is set to #{@value.inspect}"
end
def error_description
if @instance.errors.empty?
"no errors"
else
"errors: #{pretty_error_messages(@instance)}"
end
end
end
end
end

View file

@ -1,224 +0,0 @@
module Shoulda # :nodoc:
module ActiveRecord # :nodoc:
# Ensure that the belongs_to relationship exists.
#
# it { should belong_to(:parent) }
#
def belong_to(name)
AssociationMatcher.new(:belongs_to, name)
end
# Ensures that the has_many relationship exists. Will also test that the
# associated table has the required columns. Works with polymorphic
# associations.
#
# Options:
# * <tt>through</tt> - association name for <tt>has_many :through</tt>
# * <tt>dependent</tt> - tests that the association makes use of the
# dependent option.
#
# Example:
# it { should have_many(:friends) }
# it { should have_many(:enemies).through(:friends) }
# it { should have_many(:enemies).dependent(:destroy) }
#
def have_many(name)
AssociationMatcher.new(:has_many, name)
end
# Ensure that the has_one relationship exists. Will also test that the
# associated table has the required columns. Works with polymorphic
# associations.
#
# Options:
# * <tt>:dependent</tt> - tests that the association makes use of the
# dependent option.
#
# Example:
# it { should have_one(:god) } # unless hindu
#
def have_one(name)
AssociationMatcher.new(:has_one, name)
end
# Ensures that the has_and_belongs_to_many relationship exists, and that
# the join table is in place.
#
# it { should have_and_belong_to_many(:posts) }
#
def have_and_belong_to_many(name)
AssociationMatcher.new(:has_and_belongs_to_many, name)
end
class AssociationMatcher # :nodoc:
def initialize(macro, name)
@macro = macro
@name = name
end
def through(through)
@through = through
self
end
def dependent(dependent)
@dependent = dependent
self
end
def matches?(subject)
@subject = subject
association_exists? &&
macro_correct? &&
foreign_key_exists? &&
through_association_valid? &&
dependent_correct? &&
join_table_exists?
end
def failure_message
"Expected #{expectation} (#{@missing})"
end
def negative_failure_message
"Did not expect #{expectation}"
end
def description
description = "#{macro_description} #{@name}"
description += " through #{@through}" if @through
description += " dependent => #{@dependent}" if @dependent
description
end
protected
def association_exists?
if reflection.nil?
@missing = "no association called #{@name}"
false
else
true
end
end
def macro_correct?
if reflection.macro == @macro
true
else
@missing = "actual association type was #{reflection.macro}"
false
end
end
def foreign_key_exists?
!(belongs_foreign_key_missing? || has_foreign_key_missing?)
end
def belongs_foreign_key_missing?
@macro == :belongs_to && !class_has_foreign_key?(model_class)
end
def has_foreign_key_missing?
[:has_many, :has_one].include?(@macro) &&
!through? &&
!class_has_foreign_key?(associated_class)
end
def through_association_valid?
@through.nil? || (through_association_exists? && through_association_correct?)
end
def through_association_exists?
if through_reflection.nil?
@missing = "#{model_class.name} does not have any relationship to #{@through}"
false
else
true
end
end
def through_association_correct?
if @through == reflection.options[:through]
true
else
@missing = "Expected #{model_class.name} to have #{@name} through #{@through}, " <<
"but got it through #{reflection.options[:through]}"
false
end
end
def dependent_correct?
if @dependent.nil? || @dependent.to_s == reflection.options[:dependent].to_s
true
else
@missing = "#{@name} should have #{@dependent} dependency"
false
end
end
def join_table_exists?
if @macro != :has_and_belongs_to_many ||
::ActiveRecord::Base.connection.tables.include?(join_table.to_s)
true
else
@missing = "join table #{join_table} doesn't exist"
false
end
end
def class_has_foreign_key?(klass)
if klass.column_names.include?(foreign_key.to_s)
true
else
@missing = "#{klass} does not have a #{foreign_key} foreign key."
false
end
end
def model_class
@subject.class
end
def join_table
reflection.options[:join_table]
end
def associated_class
reflection.klass
end
def foreign_key
reflection.primary_key_name
end
def through?
reflection.options[:through]
end
def reflection
@reflection ||= model_class.reflect_on_association(@name)
end
def through_reflection
@through_reflection ||= model_class.reflect_on_association(@through)
end
def expectation
"#{model_class.name} to have a #{@macro} association called #{@name}"
end
def macro_description
case @macro.to_s
when 'belongs_to' then 'belong to'
when 'has_many' then 'have many'
when 'has_one' then 'have one'
when 'has_and_belongs_to_many' then
'have and belong to many'
end
end
end
end
end

View file

@ -1,85 +0,0 @@
module Shoulda # :nodoc:
module ActiveRecord # :nodoc:
# Ensure that the attribute's value is in the range specified
#
# Options:
# * <tt>in_range</tt> - the range of allowed values for this attribute
# * <tt>with_low_message</tt> - value the test expects to find in
# <tt>errors.on(:attribute)</tt>. Regexp or string. Defaults to the
# translation for :inclusion.
# * <tt>with_high_message</tt> - value the test expects to find in
# <tt>errors.on(:attribute)</tt>. Regexp or string. Defaults to the
# translation for :inclusion.
#
# Example:
# it { should ensure_inclusion_of(:age).in_range(0..100) }
#
def ensure_inclusion_of(attr)
EnsureInclusionOfMatcher.new(attr)
end
class EnsureInclusionOfMatcher < ValidationMatcher # :nodoc:
def in_range(range)
@range = range
@minimum = range.first
@maximum = range.last
self
end
def with_message(message)
if message
@low_message = message
@high_message = message
end
self
end
def with_low_message(message)
@low_message = message if message
self
end
def with_high_message(message)
@high_message = message if message
self
end
def description
"ensure inclusion of #{@attribute} in #{@range.inspect}"
end
def matches?(subject)
super(subject)
@low_message ||= :inclusion
@high_message ||= :inclusion
disallows_lower_value &&
allows_minimum_value &&
disallows_higher_value &&
allows_maximum_value
end
private
def disallows_lower_value
@minimum == 0 || disallows_value_of(@minimum - 1, @low_message)
end
def disallows_higher_value
disallows_value_of(@maximum + 1, @high_message)
end
def allows_minimum_value
allows_value_of(@minimum, @low_message)
end
def allows_maximum_value
allows_value_of(@maximum, @high_message)
end
end
end
end

View file

@ -1,139 +0,0 @@
module Shoulda # :nodoc:
module ActiveRecord # :nodoc:
# Ensures that the length of the attribute is validated.
#
# Options:
# * <tt>is_at_least</tt> - minimum length of this attribute
# * <tt>is_at_most</tt> - maximum length of this attribute
# * <tt>is_equal_to</tt> - exact requred length of this attribute
# * <tt>with_short_message</tt> - value the test expects to find in
# <tt>errors.on(:attribute)</tt>. Regexp or string. Defaults to the
# translation for :too_short.
# * <tt>with_long_message</tt> - value the test expects to find in
# <tt>errors.on(:attribute)</tt>. Regexp or string. Defaults to the
# translation for :too_long.
# * <tt>with_message</tt> - value the test expects to find in
# <tt>errors.on(:attribute)</tt>. Regexp or string. Defaults to the
# translation for :wrong_length. Used in conjunction with
# <tt>is_equal_to</tt>.
#
# Examples:
# it { should ensure_length_of(:password).
# is_at_least(6).
# is_at_most(20) }
# it { should ensure_length_of(:name).
# is_at_least(3).
# with_short_message(/not long enough/) }
# it { should ensure_length_of(:ssn).
# is_equal_to(9).
# with_message(/is invalid/) }
def ensure_length_of(attr)
EnsureLengthOfMatcher.new(attr)
end
class EnsureLengthOfMatcher < ValidationMatcher # :nodoc:
include Helpers
def is_at_least(length)
@minimum = length
@short_message ||= :too_short
self
end
def is_at_most(length)
@maximum = length
@long_message ||= :too_long
self
end
def is_equal_to(length)
@minimum = length
@maximum = length
@short_message ||= :wrong_length
self
end
def with_short_message(message)
@short_message = message if message
self
end
alias_method :with_message, :with_short_message
def with_long_message(message)
@long_message = message if message
self
end
def description
description = "ensure #{@attribute} has a length "
if @minimum && @maximum
if @minimum == @maximum
description << "of exactly #{@minimum}"
else
description << "between #{@minimum} and #{@maximum}"
end
else
description << "of at least #{@minimum}" if @minimum
description << "of at most #{@maximum}" if @maximum
end
description
end
def matches?(subject)
super(subject)
translate_messages!
disallows_lower_length &&
allows_minimum_length &&
((@minimum == @maximum) ||
(disallows_higher_length &&
allows_maximum_length))
end
private
def translate_messages!
if Symbol === @short_message
@short_message = default_error_message(@short_message,
:count => @minimum)
end
if Symbol === @long_message
@long_message = default_error_message(@long_message,
:count => @maximum)
end
end
def disallows_lower_length
@minimum == 0 ||
@minimum.nil? ||
disallows_length_of(@minimum - 1, @short_message)
end
def disallows_higher_length
@maximum.nil? || disallows_length_of(@maximum + 1, @long_message)
end
def allows_minimum_length
allows_length_of(@minimum, @short_message)
end
def allows_maximum_length
allows_length_of(@maximum, @long_message)
end
def allows_length_of(length, message)
length.nil? || allows_value_of(string_of_length(length), message)
end
def disallows_length_of(length, message)
length.nil? || disallows_value_of(string_of_length(length), message)
end
def string_of_length(length)
'x' * length
end
end
end
end

View file

@ -1,167 +0,0 @@
module Shoulda # :nodoc:
module ActiveRecord # :nodoc:
# Ensures the database column exists.
#
# Options:
# * <tt>of_type</tt> - db column type (:integer, :string, etc.)
# * <tt>with_options</tt> - same options available in migrations
# (:default, :null, :limit, :precision, :scale)
#
# Examples:
# it { should_not have_db_column(:admin).of_type(:boolean) }
# it { should have_db_column(:salary).
# of_type(:decimal).
# with_options(:precision => 10, :scale => 2) }
#
def have_db_column(column)
HaveDbColumnMatcher.new(:have_db_column, column)
end
class HaveDbColumnMatcher # :nodoc:
def initialize(macro, column)
@macro = macro
@column = column
end
def of_type(column_type)
@column_type = column_type
self
end
def with_options(opts = {})
@precision = opts[:precision]
@limit = opts[:limit]
@default = opts[:default]
@null = opts[:null]
@scale = opts[:scale]
self
end
def matches?(subject)
@subject = subject
column_exists? &&
correct_column_type? &&
correct_precision? &&
correct_limit? &&
correct_default? &&
correct_null? &&
correct_scale?
end
def failure_message
"Expected #{expectation} (#{@missing})"
end
def negative_failure_message
"Did not expect #{expectation}"
end
def description
desc = "have db column named #{@column}"
desc << " of type #{@column_type}" unless @column_type.nil?
desc << " of precision #{@precision}" unless @precision.nil?
desc << " of limit #{@limit}" unless @limit.nil?
desc << " of default #{@default}" unless @default.nil?
desc << " of null #{@null}" unless @null.nil?
desc << " of primary #{@primary}" unless @primary.nil?
desc << " of scale #{@scale}" unless @scale.nil?
desc
end
protected
def column_exists?
if model_class.column_names.include?(@column.to_s)
true
else
@missing = "#{model_class} does not have a db column named #{@column}."
false
end
end
def correct_column_type?
return true if @column_type.nil?
if matched_column.type.to_s == @column_type.to_s
true
else
@missing = "#{model_class} has a db column named #{@column} " <<
"of type #{matched_column.type}, not #{@column_type}."
false
end
end
def correct_precision?
return true if @precision.nil?
if matched_column.precision.to_s == @precision.to_s
true
else
@missing = "#{model_class} has a db column named #{@column} " <<
"of precision #{matched_column.precision}, " <<
"not #{@precision}."
false
end
end
def correct_limit?
return true if @limit.nil?
if matched_column.limit.to_s == @limit.to_s
true
else
@missing = "#{model_class} has a db column named #{@column} " <<
"of limit #{matched_column.limit}, " <<
"not #{@limit}."
false
end
end
def correct_default?
return true if @default.nil?
if matched_column.default.to_s == @default.to_s
true
else
@missing = "#{model_class} has a db column named #{@column} " <<
"of default #{matched_column.default}, " <<
"not #{@default}."
false
end
end
def correct_null?
return true if @null.nil?
if matched_column.null.to_s == @null.to_s
true
else
@missing = "#{model_class} has a db column named #{@column} " <<
"of null #{matched_column.null}, " <<
"not #{@null}."
false
end
end
def correct_scale?
return true if @scale.nil?
if matched_column.scale.to_s == @scale.to_s
true
else
@missing = "#{model_class} has a db column named #{@column} " <<
"of scale #{matched_column.scale}, not #{@scale}."
false
end
end
def matched_column
model_class.columns.detect { |each| each.name == @column.to_s }
end
def model_class
@subject.class
end
def expectation
expected = "#{model_class.name} to #{description}"
end
end
end
end

View file

@ -1,110 +0,0 @@
module Shoulda # :nodoc:
module ActiveRecord # :nodoc:
# Ensures that there are DB indices on the given columns or tuples of
# columns.
#
# Options:
# * <tt>unique</tt> - whether or not the index has a unique
# constraint. Use <tt>true</tt> to explicitly test for a unique
# constraint. Use <tt>false</tt> to explicitly test for a non-unique
# constraint. Use <tt>nil</tt> if you don't care whether the index is
# unique or not. Default = <tt>nil</tt>
#
# Examples:
#
# it { should have_db_index(:age) }
# it { should have_db_index([:commentable_type, :commentable_id]) }
# it { should have_db_index(:ssn).unique(true) }
#
def have_db_index(columns)
HaveDbIndexMatcher.new(:have_index, columns)
end
class HaveDbIndexMatcher # :nodoc:
def initialize(macro, columns)
@macro = macro
@columns = normalize_columns_to_array(columns)
end
def unique(unique)
@unique = unique
self
end
def matches?(subject)
@subject = subject
index_exists? && correct_unique?
end
def failure_message
"Expected #{expectation} (#{@missing})"
end
def negative_failure_message
"Did not expect #{expectation}"
end
def description
"have a #{index_type} index on columns #{@columns.join(' and ')}"
end
protected
def index_exists?
! matched_index.nil?
end
def correct_unique?
return true if @unique.nil?
if matched_index.unique == @unique
true
else
@missing = "#{table_name} has an index named #{matched_index.name} " <<
"of unique #{matched_index.unique}, not #{@unique}."
false
end
end
def matched_index
indexes.detect { |each| each.columns == @columns }
end
def model_class
@subject.class
end
def table_name
model_class.table_name
end
def indexes
::ActiveRecord::Base.connection.indexes(table_name)
end
def expectation
expected = "#{model_class.name} to #{description}"
end
def index_type
case @unique
when nil
''
when false
'non-unique'
else
'unique'
end
end
def normalize_columns_to_array(columns)
if columns.class == Array
columns.collect { |each| each.to_s }
else
[columns.to_s]
end
end
end
end
end

View file

@ -1,57 +0,0 @@
module Shoulda # :nodoc:
module ActiveRecord # :nodoc:
# Ensures that the attribute cannot be changed once the record has been
# created.
#
# it { should have_readonly_attributes(:password) }
#
def have_readonly_attribute(value)
HaveReadonlyAttributeMatcher.new(value)
end
class HaveReadonlyAttributeMatcher # :nodoc:
def initialize(attribute)
@attribute = attribute.to_s
end
def matches?(subject)
@subject = subject
if readonly_attributes.include?(@attribute)
@negative_failure_message =
"Did not expect #{@attribute} to be read-only"
true
else
if readonly_attributes.empty?
@failure_message = "#{class_name} attribute #{@attribute} " <<
"is not read-only"
else
@failure_message = "#{class_name} is making " <<
"#{readonly_attributes.to_sentence} " <<
"read-only, but not #{@attribute}."
end
false
end
end
attr_reader :failure_message, :negative_failure_message
def description
"make #{@attribute} read-only"
end
private
def readonly_attributes
@readonly_attributes ||= (@subject.class.readonly_attributes || [])
end
def class_name
@subject.class.name
end
end
end
end

View file

@ -1,32 +0,0 @@
module Shoulda # :nodoc:
module ActiveRecord # :nodoc:
module Helpers
def pretty_error_messages(obj) # :nodoc:
obj.errors.map do |a, m|
msg = "#{a} #{m}"
msg << " (#{obj.send(a).inspect})" unless a.to_sym == :base
end
end
# Helper method that determines the default error message used by Active
# Record. Works for both existing Rails 2.1 and Rails 2.2 with the newly
# introduced I18n module used for localization.
#
# default_error_message(:blank)
# default_error_message(:too_short, :count => 5)
# default_error_message(:too_long, :count => 60)
def default_error_message(key, values = {})
if Object.const_defined?(:I18n) # Rails >= 2.2
result = I18n.translate("activerecord.errors.messages.#{key}", values)
if result =~ /^translation missing/
I18n.translate("errors.messages.#{key}", values)
else
result
end
else # Rails <= 2.1.x
::ActiveRecord::Errors.default_error_messages[key] % values[:count]
end
end
end
end
end

View file

@ -1,39 +0,0 @@
module Shoulda # :nodoc:
module ActiveRecord # :nodoc:
# Ensures that the model cannot be saved the given attribute is not
# accepted.
#
# Options:
# * <tt>with_message</tt> - value the test expects to find in
# <tt>errors.on(:attribute)</tt>. Regexp or string. Defaults to the
# translation for <tt>:accepted</tt>.
#
# Example:
# it { should validate_acceptance_of(:eula) }
#
def validate_acceptance_of(attr)
ValidateAcceptanceOfMatcher.new(attr)
end
class ValidateAcceptanceOfMatcher < ValidationMatcher # :nodoc:
def with_message(message)
@expected_message = message if message
self
end
def matches?(subject)
super(subject)
@expected_message ||= :accepted
disallows_value_of(false, @expected_message)
end
def description
"require #{@attribute} to be accepted"
end
end
end
end

View file

@ -1,63 +0,0 @@
module Shoulda # :nodoc:
module ActiveRecord # :nodoc:
# Ensures that the model is not valid if the given attribute is not
# formatted correctly.
#
# Options:
# * <tt>with_message</tt> - value the test expects to find in
# <tt>errors.on(:attribute)</tt>. <tt>Regexp</tt> or <tt>String</tt>.
# Defaults to the translation for <tt>:blank</tt>.
# * <tt>with(string to test against)</tt>
# * <tt>not_with(string to test against)</tt>
#
# Examples:
# it { should validate_format_of(:name).
# with('12345').
# with_message(/is not optional/) }
# it { should validate_format_of(:name).
# not_with('12D45').
# with_message(/is not optional/) }
#
def validate_format_of(attr)
ValidateFormatOfMatcher.new(attr)
end
class ValidateFormatOfMatcher < ValidationMatcher # :nodoc:
def initialize(attribute)
super
end
def with_message(message)
@expected_message = message if message
self
end
def with(value)
raise "You may not call both with and not_with" if @value_to_fail
@value_to_pass = value
self
end
def not_with(value)
raise "You may not call both with and not_with" if @value_to_pass
@value_to_fail = value
self
end
def matches?(subject)
super(subject)
@expected_message ||= :blank
return disallows_value_of(@value_to_fail, @expected_message) if @value_to_fail
allows_value_of(@value_to_pass, @expected_message) if @value_to_pass
end
def description
"#{@attribute} have a valid format"
end
end
end
end

View file

@ -1,37 +0,0 @@
module Shoulda # :nodoc:
module ActiveRecord # :nodoc:
# Ensure that the attribute is numeric
#
# Options:
# * <tt>with_message</tt> - value the test expects to find in
# <tt>errors.on(:attribute)</tt>. Regexp or string. Defaults to the
# translation for <tt>:not_a_number</tt>.
#
# Example:
# it { should validate_numericality_of(:age) }
#
def validate_numericality_of(attr)
ValidateNumericalityOfMatcher.new(attr)
end
class ValidateNumericalityOfMatcher < ValidationMatcher # :nodoc:
def with_message(message)
@expected_message = message if message
self
end
def matches?(subject)
super(subject)
@expected_message ||= :not_a_number
disallows_value_of('abcd', @expected_message)
end
def description
"only allow numeric values for #{@attribute}"
end
end
end
end

View file

@ -1,58 +0,0 @@
module Shoulda # :nodoc:
module ActiveRecord # :nodoc:
# Ensures that the model is not valid if the given attribute is not
# present.
#
# Options:
# * <tt>with_message</tt> - value the test expects to find in
# <tt>errors.on(:attribute)</tt>. <tt>Regexp</tt> or <tt>String</tt>.
# Defaults to the translation for <tt>:blank</tt>.
#
# Examples:
# it { should validate_presence_of(:name) }
# it { should validate_presence_of(:name).
# with_message(/is not optional/) }
#
def validate_presence_of(attr)
ValidatePresenceOfMatcher.new(attr)
end
class ValidatePresenceOfMatcher < ValidationMatcher # :nodoc:
def with_message(message)
@expected_message = message if message
self
end
def matches?(subject)
super(subject)
@expected_message ||= :blank
disallows_value_of(blank_value, @expected_message)
end
def description
"require #{@attribute} to be set"
end
private
def blank_value
if collection?
[]
else
nil
end
end
def collection?
if reflection = @subject.class.reflect_on_association(@attribute)
[:has_many, :has_and_belongs_to_many].include?(reflection.macro)
else
false
end
end
end
end
end

View file

@ -1,146 +0,0 @@
module Shoulda # :nodoc:
module ActiveRecord # :nodoc:
# Ensures that the model is invalid if the given attribute is not unique.
#
# Internally, this uses values from existing records to test validations,
# so this will always fail if you have not saved at least one record for
# the model being tested, like so:
#
# describe User do
# before(:each) { User.create!(:email => 'address@example.com') }
# it { should validate_uniqueness_of(:email) }
# end
#
# Options:
#
# * <tt>with_message</tt> - value the test expects to find in
# <tt>errors.on(:attribute)</tt>. <tt>Regexp</tt> or <tt>String</tt>.
# Defaults to the translation for <tt>:taken</tt>.
# * <tt>scoped_to</tt> - field(s) to scope the uniqueness to.
# * <tt>case_insensitive</tt> - ensures that the validation does not
# check case. Off by default. Ignored by non-text attributes.
#
# Examples:
# it { should validate_uniqueness_of(:keyword) }
# it { should validate_uniqueness_of(:keyword).with_message(/dup/) }
# it { should validate_uniqueness_of(:email).scoped_to(:name) }
# it { should validate_uniqueness_of(:email).
# scoped_to(:first_name, :last_name) }
# it { should validate_uniqueness_of(:keyword).case_insensitive }
#
def validate_uniqueness_of(attr)
ValidateUniquenessOfMatcher.new(attr)
end
class ValidateUniquenessOfMatcher < ValidationMatcher # :nodoc:
include Helpers
def initialize(attribute)
@attribute = attribute
end
def scoped_to(*scopes)
@scopes = [*scopes].flatten
self
end
def with_message(message)
@expected_message = message
self
end
def case_insensitive
@case_insensitive = true
self
end
def description
result = "require "
result << "case sensitive " unless @case_insensitive
result << "unique value for #{@attribute}"
result << " scoped to #{@scopes.join(', ')}" unless @scopes.blank?
result
end
def matches?(subject)
@subject = subject.class.new
@expected_message ||= :taken
find_existing &&
set_scoped_attributes &&
validate_attribute &&
validate_after_scope_change
end
private
def find_existing
if @existing = @subject.class.find(:first)
true
else
@failure_message = "Can't find first #{class_name}"
false
end
end
def set_scoped_attributes
unless @scopes.blank?
@scopes.each do |scope|
setter = :"#{scope}="
unless @subject.respond_to?(setter)
@failure_message =
"#{class_name} doesn't seem to have a #{scope} attribute."
return false
end
@subject.send("#{scope}=", @existing.send(scope))
end
end
true
end
def validate_attribute
disallows_value_of(existing_value, @expected_message)
end
# TODO: There is a chance that we could change the scoped field
# to a value that's already taken. An alternative implementation
# could actually find all values for scope and create a unique
def validate_after_scope_change
if @scopes.blank?
true
else
@scopes.all? do |scope|
previous_value = @existing.send(scope)
# Assume the scope is a foreign key if the field is nil
previous_value ||= 0
next_value = previous_value.next
@subject.send("#{scope}=", next_value)
if allows_value_of(existing_value, @expected_message)
@negative_failure_message <<
" (with different value of #{scope})"
true
else
@failure_message << " (with different value of #{scope})"
false
end
end
end
end
def class_name
@subject.class.name
end
def existing_value
value = @existing.send(@attribute)
value.swapcase! if @case_insensitive && value.respond_to?(:swapcase!)
value
end
end
end
end

View file

@ -1,54 +0,0 @@
module Shoulda # :nodoc:
module ActiveRecord # :nodoc:
class ValidationMatcher # :nodoc:
attr_reader :failure_message
def initialize(attribute)
@attribute = attribute
end
def negative_failure_message
@negative_failure_message || @failure_message
end
def matches?(subject)
@subject = subject
false
end
private
def allows_value_of(value, message = nil)
allow = AllowValueMatcher.
new(value).
for(@attribute).
with_message(message)
if allow.matches?(@subject)
@negative_failure_message = allow.failure_message
true
else
@failure_message = allow.negative_failure_message
false
end
end
def disallows_value_of(value, message = nil)
disallow = AllowValueMatcher.
new(value).
for(@attribute).
with_message(message)
if disallow.matches?(@subject)
@failure_message = disallow.negative_failure_message
false
else
@negative_failure_message = disallow.failure_message
true
end
end
end
end
end

View file

@ -1,23 +0,0 @@
# :enddoc:
if defined?(::ActiveRecord)
require 'shoulda/active_record'
module RSpec::Matchers
include Shoulda::ActiveRecord
end
end
if defined?(::ActionController)
require 'shoulda/action_controller'
module RSpec::Rails::ControllerExampleGroup
include Shoulda::ActionController
end
end
if defined?(::ActionMailer)
require 'shoulda/action_mailer'
module RSpec::Rails::MailerExampleGroup
include Shoulda::ActionMailer
end
end

View file

@ -1,41 +0,0 @@
# :enddoc:
if defined?(ActionController)
require 'shoulda/action_controller'
class ActionController::TestCase
include Shoulda::ActionController
extend Shoulda::ActionController
def subject
@controller
end
end
end
if defined?(ActionMailer)
require 'shoulda/action_mailer'
module Test
module Unit
class TestCase
include Shoulda::ActionMailer
extend Shoulda::ActionMailer
end
end
end
end
if defined?(ActiveRecord)
require 'shoulda/active_record'
module Test
module Unit
class TestCase
include Shoulda::ActiveRecord
extend Shoulda::ActiveRecord
end
end
end
end

View file

@ -0,0 +1,38 @@
require 'shoulda/matchers/action_controller/assign_to_matcher'
require 'shoulda/matchers/action_controller/filter_param_matcher'
require 'shoulda/matchers/action_controller/set_the_flash_matcher'
require 'shoulda/matchers/action_controller/render_with_layout_matcher'
require 'shoulda/matchers/action_controller/respond_with_matcher'
require 'shoulda/matchers/action_controller/respond_with_content_type_matcher'
require 'shoulda/matchers/action_controller/set_session_matcher'
require 'shoulda/matchers/action_controller/route_matcher'
require 'shoulda/matchers/action_controller/redirect_to_matcher'
require 'shoulda/matchers/action_controller/render_template_matcher'
module Shoulda
module Matchers
# By using the matchers you can quickly and easily create concise and
# easy to read test suites.
#
# This code segment:
#
# describe UsersController, "on GET to show with a valid id" do
# before(:each) do
# get :show, :id => User.first.to_param
# end
#
# it { should assign_to(:user) }
# it { should respond_with(:success) }
# it { should render_template(:show) }
# it { should not_set_the_flash) }
#
# it "should do something else really cool" do
# assigns[:user].id.should == 1
# end
# end
#
# Would produce 5 tests for the show action
module ActionController
end
end
end

View file

@ -0,0 +1,114 @@
module Shoulda # :nodoc:
module Matchers
module ActionController # :nodoc:
# Ensures that the controller assigned to the named instance variable.
#
# Options:
# * <tt>with_kind_of</tt> - The expected class of the instance variable
# being checked.
# * <tt>with</tt> - The value that should be assigned.
#
# Example:
#
# it { should assign_to(:user) }
# it { should_not assign_to(:user) }
# it { should assign_to(:user).with_kind_of(User) }
# it { should assign_to(:user).with(@user) }
def assign_to(variable)
AssignToMatcher.new(variable)
end
class AssignToMatcher # :nodoc:
def initialize(variable)
@variable = variable.to_s
@check_value = false
end
def with_kind_of(expected_class)
@expected_class = expected_class
self
end
def with(expected_value = nil, &block)
@check_value = true
@expected_value = expected_value
@expectation_block = block
self
end
def matches?(controller)
@controller = controller
@expected_value = @context.instance_eval(&@expectation_block) if @expectation_block
assigned_value? && kind_of_expected_class? && equal_to_expected_value?
end
attr_reader :failure_message, :negative_failure_message
def description
description = "assign @#{@variable}"
description << " with a kind of #{@expected_class}" if @expected_class
description
end
def in_context(context)
@context = context
self
end
private
def assigned_value?
if !@controller.instance_variables.include?("@#{@variable}")
@failure_message =
"Expected action to assign a value for @#{@variable}"
false
else
@negative_failure_message =
"Didn't expect action to assign a value for @#{@variable}, " <<
"but it was assigned to #{assigned_value.inspect}"
true
end
end
def kind_of_expected_class?
return true unless @expected_class
if assigned_value.kind_of?(@expected_class)
@negative_failure_message =
"Didn't expect action to assign a kind of #{@expected_class} " <<
"for #{@variable}, but got one anyway"
true
else
@failure_message =
"Expected action to assign a kind of #{@expected_class} " <<
"for #{@variable}, but got #{@variable.inspect} " <<
"(#{@variable.class.name})"
false
end
end
def equal_to_expected_value?
return true unless @check_value
if @expected_value == assigned_value
@negative_failure_message =
"Didn't expect action to assign #{@expected_value.inspect} " <<
"for #{@variable}, but got it anyway"
true
else
@failure_message =
"Expected action to assign #{@expected_value.inspect} " <<
"for #{@variable}, but got #{assigned_value.inspect}"
false
end
end
def assigned_value
@controller.instance_variable_get("@#{@variable}")
end
end
end
end
end

View file

@ -0,0 +1,50 @@
module Shoulda # :nodoc:
module Matchers
module ActionController # :nodoc:
# Ensures that filter_parameter_logging is set for the specified key.
#
# Example:
#
# it { should filter_param(:password) }
def filter_param(key)
FilterParamMatcher.new(key)
end
class FilterParamMatcher # :nodoc:
def initialize(key)
@key = key.to_s
end
def matches?(controller)
@controller = controller
filters_key?
end
def failure_message
"Expected #{@key} to be filtered; filtered keys: #{filtered_keys.join(', ')}"
end
def negative_failure_message
"Did not expect #{@key} to be filtered"
end
def description
"filter #{@key}"
end
private
def filters_key?
filtered_keys.include?(@key)
end
def filtered_keys
Rails.application.config.filter_parameters.map { |filter| filter.to_s }
end
end
end
end
end

View file

@ -0,0 +1,62 @@
module Shoulda # :nodoc:
module Matchers
module ActionController # :nodoc:
# Ensures a controller redirected to the given url.
#
# Example:
#
# it { should redirect_to('http://somewhere.com') }
# it { should redirect_to(users_path) }
def redirect_to(url_or_description, &block)
RedirectToMatcher.new(url_or_description, self, &block)
end
class RedirectToMatcher # :nodoc:
def initialize(url_or_description, context, &block)
if block
@url_block = block
@location = @url_or_description
else
@url = url_or_description
@location = @url
end
@context = context
end
def in_context(context)
@context = context
self
end
def matches?(controller)
@controller = controller
redirects_to_url?
end
attr_reader :failure_message, :negative_failure_message
def description
"redirect to #{@location}"
end
private
def redirects_to_url?
@url = @context.instance_eval(&@url_block) if @url_block
begin
@context.send(:assert_redirected_to, @url)
@negative_failure_message = "Didn't expect to redirect to #{@url}"
true
rescue Test::Unit::AssertionFailedError => error
@failure_message = error.message
false
end
end
end
end
end
end

View file

@ -0,0 +1,54 @@
module Shoulda # :nodoc:
module Matchers
module ActionController # :nodoc:
# Ensures a controller rendered the given template.
#
# Example:
#
# it { should render_template(:show) }
def render_template(template)
RenderTemplateMatcher.new(template, self)
end
class RenderTemplateMatcher # :nodoc:
def initialize(template, context)
@template = template.to_s
@context = context
end
def matches?(controller)
@controller = controller
renders_template?
end
attr_reader :failure_message, :negative_failure_message
def description
"render template #{@template}"
end
def in_context(context)
@context = context
self
end
private
def renders_template?
begin
@context.send(:assert_template, @template)
@negative_failure_message = "Didn't expect to render #{@template}"
true
rescue Test::Unit::AssertionFailedError => error
@failure_message = error.message
false
end
end
end
end
end
end

View file

@ -0,0 +1,99 @@
module Shoulda # :nodoc:
module Matchers
module ActionController # :nodoc:
# Ensures that the controller rendered with the given layout.
#
# Example:
#
# it { should render_with_layout }
# it { should render_with_layout(:special) }
# it { should_not render_with_layout }
def render_with_layout(expected_layout = nil)
RenderWithLayoutMatcher.new(expected_layout).in_context(self)
end
class RenderWithLayoutMatcher # :nodoc:
def initialize(expected_layout)
@expected_layout = expected_layout.to_s unless expected_layout.nil?
end
# Used to provide access to layouts recorded by
# ActionController::TemplateAssertions in Rails 3
def in_context(context)
@context = context
self
end
def matches?(controller)
@controller = controller
rendered_with_layout? && rendered_with_expected_layout?
end
def failure_message
"Expected #{expectation}, but #{result}"
end
def negative_failure_message
"Did not expect #{expectation}, but #{result}"
end
def description
description = "render with "
if @expected_layout.nil?
description << "a layout"
else
description << "the #{@expected_layout.inspect} layout"
end
description
end
private
def rendered_with_layout?
!rendered_layouts.empty?
end
def rendered_with_expected_layout?
return true if @expected_layout.nil?
rendered_layouts.include?(@expected_layout)
end
def rendered_layouts
if recorded_layouts
recorded_layouts.keys.compact.map { |layout| layout.sub(%r{^layouts/}, '') }
else
layout = @controller.response.layout
if layout.nil?
[]
else
[layout.split('/').last]
end
end
end
def recorded_layouts
if @context
@context.instance_variable_get('@layouts')
end
end
def expectation
"to #{description}"
end
def result
if rendered_with_layout?
"rendered with " <<
rendered_layouts.map { |layout| layout.inspect }.join(", ")
else
"rendered without a layout"
end
end
end
end
end
end

View file

@ -0,0 +1,74 @@
module Shoulda # :nodoc:
module Matchers
module ActionController # :nodoc:
# Ensures a controller responded with expected 'response' content type.
#
# You can pass an explicit content type such as 'application/rss+xml'
# or its symbolic equivalent :rss
# or a regular expression such as /rss/
#
# Example:
#
# it { should respond_with_content_type(:xml) }
# it { should respond_with_content_type(:csv) }
# it { should respond_with_content_type(:atom) }
# it { should respond_with_content_type(:yaml) }
# it { should respond_with_content_type(:text) }
# it { should respond_with_content_type('application/rss+xml') }
# it { should respond_with_content_type(/json/) }
def respond_with_content_type(content_type)
RespondWithContentTypeMatcher.new(content_type)
end
class RespondWithContentTypeMatcher # :nodoc:
def initialize(content_type)
@content_type = if content_type.is_a?(Symbol)
lookup_by_extension(content_type)
else
content_type
end
end
def description
"respond with content type of #{@content_type}"
end
def matches?(controller)
@controller = controller
if @content_type.is_a?(Regexp)
response_content_type =~ @content_type
else
response_content_type == @content_type
end
end
def failure_message
"Expected #{expectation}"
end
def negative_failure_message
"Did not expect #{expectation}"
end
protected
def response_content_type
@controller.response.content_type.to_s
end
def lookup_by_extension(extension)
Mime::Type.lookup_by_extension(extension.to_s).to_s
end
def expectation
"content type to be #{@content_type}, " <<
"but was #{response_content_type}"
end
end
end
end
end

View file

@ -0,0 +1,85 @@
module Shoulda # :nodoc:
module Matchers
module ActionController # :nodoc:
# Ensures a controller responded with expected 'response' status code.
#
# You can pass an explicit status number like 200, 301, 404, 500
# or its symbolic equivalent :success, :redirect, :missing, :error.
# See ActionController::StatusCodes for a full list.
#
# Example:
#
# it { should respond_with(:success) }
# it { should respond_with(:redirect) }
# it { should respond_with(:missing) }
# it { should respond_with(:error) }
# it { should respond_with(501) }
def respond_with(status)
RespondWithMatcher.new(status)
end
class RespondWithMatcher # :nodoc:
def initialize(status)
@status = symbol_to_status_code(status)
end
def matches?(controller)
@controller = controller
correct_status_code? || correct_status_code_range?
end
def failure_message
"Expected #{expectation}"
end
def negative_failure_message
"Did not expect #{expectation}"
end
def description
"respond with #{@status}"
end
protected
def correct_status_code?
response_code == @status
end
def correct_status_code_range?
@status.is_a?(Range) &&
@status.include?(response_code)
end
def response_code
@controller.response.response_code
end
def symbol_to_status_code(potential_symbol)
case potential_symbol
when :success then 200
when :redirect then 300..399
when :missing then 404
when :error then 500..599
when Symbol
if defined?(::Rack::Utils::SYMBOL_TO_STATUS_CODE)
::Rack::Utils::SYMBOL_TO_STATUS_CODE[potential_symbol]
else
::ActionController::Base::SYMBOL_TO_STATUS_CODE[potential_symbol]
end
else
potential_symbol
end
end
def expectation
"response to be a #{@status}, but was #{response_code}"
end
end
end
end
end

View file

@ -0,0 +1,93 @@
module Shoulda # :nodoc:
module Matchers
module ActionController # :nodoc:
# Ensures that requesting +path+ using +method+ routes to +options+.
#
# If you don't specify a controller, it will use the controller from the
# example group.
#
# +to_param+ is called on the +options+ given.
#
# Examples:
#
# it { should route(:get, "/posts").
# to(:controller => :posts, :action => :index) }
# it { should route(:get, "/posts/new").to(:action => :new) }
# it { should route(:post, "/posts").to(:action => :create) }
# it { should route(:get, "/posts/1").to(:action => :show, :id => 1) }
# it { should route(:edit, "/posts/1").to(:action => :show, :id => 1) }
# it { should route(:put, "/posts/1").to(:action => :update, :id => 1) }
# it { should route(:delete, "/posts/1").
# to(:action => :destroy, :id => 1) }
# it { should route(:get, "/users/1/posts/1").
# to(:action => :show, :id => 1, :user_id => 1) }
def route(method, path)
RouteMatcher.new(method, path, self)
end
class RouteMatcher # :nodoc:
def initialize(method, path, context)
@method = method
@path = path
@context = context
end
def to(params)
@params = params
stringify_params!
self
end
def in_context(context)
@context = context
self
end
def matches?(controller)
@controller = controller
guess_controller!
route_recognized?
end
attr_reader :failure_message, :negative_failure_message
def description
"route #{@method.to_s.upcase} #{@path} to/from #{@params.inspect}"
end
private
def guess_controller!
@params[:controller] ||= @controller.controller_path
end
def stringify_params!
@params.each do |key, value|
@params[key] = value.is_a?(Array) ? value.collect {|v| v.to_param } : value.to_param
end
end
def route_recognized?
begin
@context.send(:assert_routing,
{ :method => @method, :path => @path },
@params)
@negative_failure_message = "Didn't expect to #{description}"
true
rescue ::ActionController::RoutingError => error
@failure_message = error.message
false
rescue Test::Unit::AssertionFailedError => error
@failure_message = error.message
false
end
end
end
end
end
end

View file

@ -0,0 +1,98 @@
module Shoulda # :nodoc:
module Matchers
module ActionController # :nodoc:
# Ensures that a session key was set to the expected value.
#
# Example:
#
# it { should set_session(:message) }
# it { should set_session(:user_id).to(@user.id) }
# it { should_not set_session(:user_id) }
def set_session(key)
SetSessionMatcher.new(key)
end
class SetSessionMatcher # :nodoc:
def initialize(key)
@key = key.to_s
end
def to(value = nil, &block)
@value = value
@value_block = block
self
end
def matches?(controller)
@controller = controller
@value = @context.instance_eval(&@value_block) if @value_block
(assigned_value? && assigned_correct_value?) || cleared_value?
end
def failure_message
"Expected #{expectation}, but #{result}"
end
def negative_failure_message
"Didn't expect #{expectation}, but #{result}"
end
def description
description = "set session variable #{@key.inspect}"
description << " to #{@value.inspect}" if defined?(@value)
description
end
def in_context(context)
@context = context
self
end
private
def assigned_value?
!assigned_value.nil?
end
def cleared_value?
defined?(@value) && @value.nil? && assigned_value.nil?
end
def assigned_correct_value?
return true if @value.nil?
assigned_value == @value
end
def assigned_value
session[@key]
end
def session
if @controller.request.respond_to?(:session)
@controller.request.session.to_hash
else
@controller.response.session.data
end
end
def expectation
expectation = "session variable #{@key} to be set"
expectation << " to #{@value.inspect}" if @value
expectation
end
def result
if session.empty?
"no session variables were set"
else
"the session was #{session.inspect}"
end
end
end
end
end
end

View file

@ -0,0 +1,94 @@
module Shoulda # :nodoc:
module Matchers
module ActionController # :nodoc:
# Ensures that the flash contains the given value. Can be a String, a
# Regexp, or nil (indicating that the flash should not be set).
#
# Example:
#
# it { should set_the_flash }
# it { should set_the_flash.to("Thank you for placing this order.") }
# it { should set_the_flash.to(/created/i) }
# it { should set_the_flash.to(/logged in/i).now }
# it { should_not set_the_flash }
def set_the_flash
SetTheFlashMatcher.new
end
class SetTheFlashMatcher # :nodoc:
def to(value)
@value = value
self
end
def now
@now = true
self
end
def matches?(controller)
@controller = controller
sets_the_flash? && string_value_matches? && regexp_value_matches?
end
attr_reader :failure_message, :negative_failure_message
def description
description = "set the flash"
description << " to #{@value.inspect}" unless @value.nil?
description
end
def failure_message
"Expected #{expectation}"
end
def negative_failure_message
"Did not expect #{expectation}"
end
private
def sets_the_flash?
!flash.blank?
end
def string_value_matches?
return true unless String === @value
flash.values.any? {|value| value == @value }
end
def regexp_value_matches?
return true unless Regexp === @value
flash.values.any? {|value| value =~ @value }
end
def flash
return @flash if @flash
@flash = @controller.flash.dup
@flash.sweep unless @now
@flash
end
def expectation
expectation = "the flash#{".now" if @now} to be set"
expectation << " to #{@value.inspect}" unless @value.nil?
expectation << ", but #{flash_description}"
expectation
end
def flash_description
if flash.blank?
"no flash was set"
else
"was #{flash.inspect}"
end
end
end
end
end
end

View file

@ -0,0 +1,22 @@
require 'shoulda/matchers/action_mailer/have_sent_email'
module Shoulda
module Matchers
# = Matchers for your mailers
#
# This matcher will test that email is sent properly
#
# describe User do
# it { should have_sent_email.with_subject(/is spam$/) }
# it { should have_sent_email.from('do-not-reply@example.com') }
# it { should have_sent_email.with_body(/is spam\./) }
# it { should have_sent_email.to('myself@me.com') }
# it { should have_sent_email.with_subject(/spam/).
# from('do-not-reply@example.com').
# with_body(/spam/).
# to('myself@me.com') }
# end
module ActionMailer
end
end
end

View file

@ -0,0 +1,110 @@
module Shoulda # :nodoc:
module Matchers
module ActionMailer # :nodoc:
# The right email is sent.
#
# it { should have_sent_email.with_subject(/is spam$/) }
# it { should have_sent_email.from('do-not-reply@example.com') }
# it { should have_sent_email.with_body(/is spam\./) }
# it { should have_sent_email.to('myself@me.com') }
# it { should have_sent_email.with_subject(/spam/).
# from('do-not-reply@example.com').
# with_body(/spam/).
# to('myself@me.com') }
def have_sent_email
HaveSentEmailMatcher.new
end
class HaveSentEmailMatcher # :nodoc:
def initialize
end
def with_subject(email_subject)
@email_subject = email_subject
self
end
def from(sender)
@sender = sender
self
end
def with_body(body)
@body = body
self
end
def to(recipient)
@recipient = recipient
self
end
def matches?(subject)
::ActionMailer::Base.deliveries.each do |mail|
@subject_failed = !regexp_or_string_match(mail.subject, @email_subject) if @email_subject
@body_failed = !regexp_or_string_match(mail.body, @body) if @body
@sender_failed = !regexp_or_string_match_in_array(mail.from, @sender) if @sender
@recipient_failed = !regexp_or_string_match_in_array(mail.to, @recipient) if @recipient
return true unless anything_failed?
end
false
end
def failure_message
"Expected #{expectation}"
end
def negative_failure_message
"Did not expect #{expectation}"
end
def description
"send an email"
end
private
def expectation
expectation = "sent email"
expectation << " with subject #{@email_subject.inspect}" if @subject_failed
expectation << " with body #{@body.inspect}" if @body_failed
expectation << " from #{@sender.inspect}" if @sender_failed
expectation << " to #{@recipient.inspect}" if @recipient_failed
expectation << "\nDeliveries:\n#{inspect_deliveries}"
end
def inspect_deliveries
::ActionMailer::Base.deliveries.map do |delivery|
"#{delivery.subject.inspect} to #{delivery.to.inspect}"
end.join("\n")
end
def anything_failed?
@subject_failed || @body_failed || @sender_failed || @recipient_failed
end
def regexp_or_string_match(a_string, a_regexp_or_string)
case a_regexp_or_string
when Regexp
a_string =~ a_regexp_or_string
when String
a_string == a_regexp_or_string
end
end
def regexp_or_string_match_in_array(an_array, a_regexp_or_string)
case a_regexp_or_string
when Regexp
an_array.any? { |string| string =~ a_regexp_or_string }
when String
an_array.include?(a_regexp_or_string)
end
end
end
end
end
end

View file

@ -0,0 +1,42 @@
require 'shoulda/matchers/active_record/helpers'
require 'shoulda/matchers/active_record/validation_matcher'
require 'shoulda/matchers/active_record/allow_value_matcher'
require 'shoulda/matchers/active_record/ensure_length_of_matcher'
require 'shoulda/matchers/active_record/ensure_inclusion_of_matcher'
require 'shoulda/matchers/active_record/validate_presence_of_matcher'
require 'shoulda/matchers/active_record/validate_format_of_matcher'
require 'shoulda/matchers/active_record/validate_uniqueness_of_matcher'
require 'shoulda/matchers/active_record/validate_acceptance_of_matcher'
require 'shoulda/matchers/active_record/validate_numericality_of_matcher'
require 'shoulda/matchers/active_record/association_matcher'
require 'shoulda/matchers/active_record/have_db_column_matcher'
require 'shoulda/matchers/active_record/have_db_index_matcher'
require 'shoulda/matchers/active_record/have_readonly_attribute_matcher'
require 'shoulda/matchers/active_record/allow_mass_assignment_of_matcher'
module Shoulda
module Matchers
# = Matchers for your active record models
#
# These matchers will test most of the validations and associations for your
# ActiveRecord models.
#
# describe User do
# it { should validate_presence_of(:name) }
# it { should validate_presence_of(:phone_number) }
# %w(abcd 1234).each do |value|
# it { should_not allow_value(value).for(:phone_number) }
# end
# it { should allow_value("(123) 456-7890").for(:phone_number) }
# it { should_not allow_mass_assignment_of(:password) }
# it { should have_one(:profile) }
# it { should have_many(:dogs) }
# it { should have_many(:messes).through(:dogs) }
# it { should belong_to(:lover) }
# end
#
module ActiveRecord
end
end
end

View file

@ -0,0 +1,83 @@
module Shoulda # :nodoc:
module Matchers
module ActiveRecord # :nodoc:
# Ensures that the attribute can be set on mass update.
#
# it { should_not allow_mass_assignment_of(:password) }
# it { should allow_mass_assignment_of(:first_name) }
#
def allow_mass_assignment_of(value)
AllowMassAssignmentOfMatcher.new(value)
end
class AllowMassAssignmentOfMatcher # :nodoc:
def initialize(attribute)
@attribute = attribute.to_s
end
def matches?(subject)
@subject = subject
if attr_mass_assignable?
if whitelisting?
@negative_failure_message = "#{@attribute} was made accessible"
else
if protected_attributes.empty?
@negative_failure_message = "no attributes were protected"
else
@negative_failure_message = "#{class_name} is protecting " <<
"#{protected_attributes.to_a.to_sentence}, " <<
"but not #{@attribute}."
end
end
true
else
if whitelisting?
@failure_message =
"Expected #{@attribute} to be accessible"
else
@failure_message =
"Did not expect #{@attribute} to be protected"
end
false
end
end
attr_reader :failure_message, :negative_failure_message
def description
"allow mass assignment of #{@attribute}"
end
private
def protected_attributes
@protected_attributes ||= (@subject.class.protected_attributes || [])
end
def accessible_attributes
@accessible_attributes ||= (@subject.class.accessible_attributes || [])
end
def whitelisting?
!accessible_attributes.empty?
end
def attr_mass_assignable?
if whitelisting?
accessible_attributes.include?(@attribute)
else
!protected_attributes.include?(@attribute)
end
end
def class_name
@subject.class.name
end
end
end
end
end

View file

@ -0,0 +1,110 @@
module Shoulda # :nodoc:
module Matchers
module ActiveRecord # :nodoc:
# Ensures that the attribute can be set to the given value.
#
# Options:
# * <tt>with_message</tt> - value the test expects to find in
# <tt>errors.on(:attribute)</tt>. Regexp or string. If omitted,
# the test looks for any errors in <tt>errors.on(:attribute)</tt>.
#
# Example:
# it { should_not allow_value('bad').for(:isbn) }
# it { should allow_value("isbn 1 2345 6789 0").for(:isbn) }
#
def allow_value(value)
AllowValueMatcher.new(value)
end
class AllowValueMatcher # :nodoc:
include Helpers
def initialize(value)
@value = value
end
def for(attribute)
@attribute = attribute
self
end
def with_message(message)
@expected_message = message if message
self
end
def matches?(instance)
@instance = instance
if Symbol === @expected_message
@expected_message = default_error_message(@expected_message)
end
@instance.send("#{@attribute}=", @value)
!errors_match?
end
def failure_message
"Did not expect #{expectation}, got error: #{@matched_error}"
end
def negative_failure_message
"Expected #{expectation}, got #{error_description}"
end
def description
"allow #{@attribute} to be set to #{@value.inspect}"
end
private
def errors_match?
@instance.valid?
@errors = errors_for_attribute(@instance, @attribute)
@errors = [@errors] unless @errors.is_a?(Array)
@expected_message ? (errors_match_regexp? || errors_match_string?) : (@errors.compact.any?)
end
def errors_for_attribute(instance, attribute)
if instance.errors.respond_to?(:[])
instance.errors[attribute]
else
instance.errors.on(attribute)
end
end
def errors_match_regexp?
if Regexp === @expected_message
@matched_error = @errors.detect { |e| e =~ @expected_message }
!@matched_error.nil?
else
false
end
end
def errors_match_string?
if @errors.include?(@expected_message)
@matched_error = @expected_message
true
else
false
end
end
def expectation
"errors " <<
(@expected_message ? "to include #{@expected_message.inspect} " : "") <<
"when #{@attribute} is set to #{@value.inspect}"
end
def error_description
if @instance.errors.empty?
"no errors"
else
"errors: #{pretty_error_messages(@instance)}"
end
end
end
end
end
end

View file

@ -0,0 +1,226 @@
module Shoulda # :nodoc:
module Matchers
module ActiveRecord # :nodoc:
# Ensure that the belongs_to relationship exists.
#
# it { should belong_to(:parent) }
#
def belong_to(name)
AssociationMatcher.new(:belongs_to, name)
end
# Ensures that the has_many relationship exists. Will also test that the
# associated table has the required columns. Works with polymorphic
# associations.
#
# Options:
# * <tt>through</tt> - association name for <tt>has_many :through</tt>
# * <tt>dependent</tt> - tests that the association makes use of the
# dependent option.
#
# Example:
# it { should have_many(:friends) }
# it { should have_many(:enemies).through(:friends) }
# it { should have_many(:enemies).dependent(:destroy) }
#
def have_many(name)
AssociationMatcher.new(:has_many, name)
end
# Ensure that the has_one relationship exists. Will also test that the
# associated table has the required columns. Works with polymorphic
# associations.
#
# Options:
# * <tt>:dependent</tt> - tests that the association makes use of the
# dependent option.
#
# Example:
# it { should have_one(:god) } # unless hindu
#
def have_one(name)
AssociationMatcher.new(:has_one, name)
end
# Ensures that the has_and_belongs_to_many relationship exists, and that
# the join table is in place.
#
# it { should have_and_belong_to_many(:posts) }
#
def have_and_belong_to_many(name)
AssociationMatcher.new(:has_and_belongs_to_many, name)
end
class AssociationMatcher # :nodoc:
def initialize(macro, name)
@macro = macro
@name = name
end
def through(through)
@through = through
self
end
def dependent(dependent)
@dependent = dependent
self
end
def matches?(subject)
@subject = subject
association_exists? &&
macro_correct? &&
foreign_key_exists? &&
through_association_valid? &&
dependent_correct? &&
join_table_exists?
end
def failure_message
"Expected #{expectation} (#{@missing})"
end
def negative_failure_message
"Did not expect #{expectation}"
end
def description
description = "#{macro_description} #{@name}"
description += " through #{@through}" if @through
description += " dependent => #{@dependent}" if @dependent
description
end
protected
def association_exists?
if reflection.nil?
@missing = "no association called #{@name}"
false
else
true
end
end
def macro_correct?
if reflection.macro == @macro
true
else
@missing = "actual association type was #{reflection.macro}"
false
end
end
def foreign_key_exists?
!(belongs_foreign_key_missing? || has_foreign_key_missing?)
end
def belongs_foreign_key_missing?
@macro == :belongs_to && !class_has_foreign_key?(model_class)
end
def has_foreign_key_missing?
[:has_many, :has_one].include?(@macro) &&
!through? &&
!class_has_foreign_key?(associated_class)
end
def through_association_valid?
@through.nil? || (through_association_exists? && through_association_correct?)
end
def through_association_exists?
if through_reflection.nil?
@missing = "#{model_class.name} does not have any relationship to #{@through}"
false
else
true
end
end
def through_association_correct?
if @through == reflection.options[:through]
true
else
@missing = "Expected #{model_class.name} to have #{@name} through #{@through}, " <<
"but got it through #{reflection.options[:through]}"
false
end
end
def dependent_correct?
if @dependent.nil? || @dependent.to_s == reflection.options[:dependent].to_s
true
else
@missing = "#{@name} should have #{@dependent} dependency"
false
end
end
def join_table_exists?
if @macro != :has_and_belongs_to_many ||
::ActiveRecord::Base.connection.tables.include?(join_table.to_s)
true
else
@missing = "join table #{join_table} doesn't exist"
false
end
end
def class_has_foreign_key?(klass)
if klass.column_names.include?(foreign_key.to_s)
true
else
@missing = "#{klass} does not have a #{foreign_key} foreign key."
false
end
end
def model_class
@subject.class
end
def join_table
reflection.options[:join_table]
end
def associated_class
reflection.klass
end
def foreign_key
reflection.primary_key_name
end
def through?
reflection.options[:through]
end
def reflection
@reflection ||= model_class.reflect_on_association(@name)
end
def through_reflection
@through_reflection ||= model_class.reflect_on_association(@through)
end
def expectation
"#{model_class.name} to have a #{@macro} association called #{@name}"
end
def macro_description
case @macro.to_s
when 'belongs_to' then 'belong to'
when 'has_many' then 'have many'
when 'has_one' then 'have one'
when 'has_and_belongs_to_many' then
'have and belong to many'
end
end
end
end
end
end

View file

@ -0,0 +1,87 @@
module Shoulda # :nodoc:
module Matchers
module ActiveRecord # :nodoc:
# Ensure that the attribute's value is in the range specified
#
# Options:
# * <tt>in_range</tt> - the range of allowed values for this attribute
# * <tt>with_low_message</tt> - value the test expects to find in
# <tt>errors.on(:attribute)</tt>. Regexp or string. Defaults to the
# translation for :inclusion.
# * <tt>with_high_message</tt> - value the test expects to find in
# <tt>errors.on(:attribute)</tt>. Regexp or string. Defaults to the
# translation for :inclusion.
#
# Example:
# it { should ensure_inclusion_of(:age).in_range(0..100) }
#
def ensure_inclusion_of(attr)
EnsureInclusionOfMatcher.new(attr)
end
class EnsureInclusionOfMatcher < ValidationMatcher # :nodoc:
def in_range(range)
@range = range
@minimum = range.first
@maximum = range.last
self
end
def with_message(message)
if message
@low_message = message
@high_message = message
end
self
end
def with_low_message(message)
@low_message = message if message
self
end
def with_high_message(message)
@high_message = message if message
self
end
def description
"ensure inclusion of #{@attribute} in #{@range.inspect}"
end
def matches?(subject)
super(subject)
@low_message ||= :inclusion
@high_message ||= :inclusion
disallows_lower_value &&
allows_minimum_value &&
disallows_higher_value &&
allows_maximum_value
end
private
def disallows_lower_value
@minimum == 0 || disallows_value_of(@minimum - 1, @low_message)
end
def disallows_higher_value
disallows_value_of(@maximum + 1, @high_message)
end
def allows_minimum_value
allows_value_of(@minimum, @low_message)
end
def allows_maximum_value
allows_value_of(@maximum, @high_message)
end
end
end
end
end

View file

@ -0,0 +1,141 @@
module Shoulda # :nodoc:
module Matchers
module ActiveRecord # :nodoc:
# Ensures that the length of the attribute is validated.
#
# Options:
# * <tt>is_at_least</tt> - minimum length of this attribute
# * <tt>is_at_most</tt> - maximum length of this attribute
# * <tt>is_equal_to</tt> - exact requred length of this attribute
# * <tt>with_short_message</tt> - value the test expects to find in
# <tt>errors.on(:attribute)</tt>. Regexp or string. Defaults to the
# translation for :too_short.
# * <tt>with_long_message</tt> - value the test expects to find in
# <tt>errors.on(:attribute)</tt>. Regexp or string. Defaults to the
# translation for :too_long.
# * <tt>with_message</tt> - value the test expects to find in
# <tt>errors.on(:attribute)</tt>. Regexp or string. Defaults to the
# translation for :wrong_length. Used in conjunction with
# <tt>is_equal_to</tt>.
#
# Examples:
# it { should ensure_length_of(:password).
# is_at_least(6).
# is_at_most(20) }
# it { should ensure_length_of(:name).
# is_at_least(3).
# with_short_message(/not long enough/) }
# it { should ensure_length_of(:ssn).
# is_equal_to(9).
# with_message(/is invalid/) }
def ensure_length_of(attr)
EnsureLengthOfMatcher.new(attr)
end
class EnsureLengthOfMatcher < ValidationMatcher # :nodoc:
include Helpers
def is_at_least(length)
@minimum = length
@short_message ||= :too_short
self
end
def is_at_most(length)
@maximum = length
@long_message ||= :too_long
self
end
def is_equal_to(length)
@minimum = length
@maximum = length
@short_message ||= :wrong_length
self
end
def with_short_message(message)
@short_message = message if message
self
end
alias_method :with_message, :with_short_message
def with_long_message(message)
@long_message = message if message
self
end
def description
description = "ensure #{@attribute} has a length "
if @minimum && @maximum
if @minimum == @maximum
description << "of exactly #{@minimum}"
else
description << "between #{@minimum} and #{@maximum}"
end
else
description << "of at least #{@minimum}" if @minimum
description << "of at most #{@maximum}" if @maximum
end
description
end
def matches?(subject)
super(subject)
translate_messages!
disallows_lower_length &&
allows_minimum_length &&
((@minimum == @maximum) ||
(disallows_higher_length &&
allows_maximum_length))
end
private
def translate_messages!
if Symbol === @short_message
@short_message = default_error_message(@short_message,
:count => @minimum)
end
if Symbol === @long_message
@long_message = default_error_message(@long_message,
:count => @maximum)
end
end
def disallows_lower_length
@minimum == 0 ||
@minimum.nil? ||
disallows_length_of(@minimum - 1, @short_message)
end
def disallows_higher_length
@maximum.nil? || disallows_length_of(@maximum + 1, @long_message)
end
def allows_minimum_length
allows_length_of(@minimum, @short_message)
end
def allows_maximum_length
allows_length_of(@maximum, @long_message)
end
def allows_length_of(length, message)
length.nil? || allows_value_of(string_of_length(length), message)
end
def disallows_length_of(length, message)
length.nil? || disallows_value_of(string_of_length(length), message)
end
def string_of_length(length)
'x' * length
end
end
end
end
end

View file

@ -0,0 +1,169 @@
module Shoulda # :nodoc:
module Matchers
module ActiveRecord # :nodoc:
# Ensures the database column exists.
#
# Options:
# * <tt>of_type</tt> - db column type (:integer, :string, etc.)
# * <tt>with_options</tt> - same options available in migrations
# (:default, :null, :limit, :precision, :scale)
#
# Examples:
# it { should_not have_db_column(:admin).of_type(:boolean) }
# it { should have_db_column(:salary).
# of_type(:decimal).
# with_options(:precision => 10, :scale => 2) }
#
def have_db_column(column)
HaveDbColumnMatcher.new(:have_db_column, column)
end
class HaveDbColumnMatcher # :nodoc:
def initialize(macro, column)
@macro = macro
@column = column
end
def of_type(column_type)
@column_type = column_type
self
end
def with_options(opts = {})
@precision = opts[:precision]
@limit = opts[:limit]
@default = opts[:default]
@null = opts[:null]
@scale = opts[:scale]
self
end
def matches?(subject)
@subject = subject
column_exists? &&
correct_column_type? &&
correct_precision? &&
correct_limit? &&
correct_default? &&
correct_null? &&
correct_scale?
end
def failure_message
"Expected #{expectation} (#{@missing})"
end
def negative_failure_message
"Did not expect #{expectation}"
end
def description
desc = "have db column named #{@column}"
desc << " of type #{@column_type}" unless @column_type.nil?
desc << " of precision #{@precision}" unless @precision.nil?
desc << " of limit #{@limit}" unless @limit.nil?
desc << " of default #{@default}" unless @default.nil?
desc << " of null #{@null}" unless @null.nil?
desc << " of primary #{@primary}" unless @primary.nil?
desc << " of scale #{@scale}" unless @scale.nil?
desc
end
protected
def column_exists?
if model_class.column_names.include?(@column.to_s)
true
else
@missing = "#{model_class} does not have a db column named #{@column}."
false
end
end
def correct_column_type?
return true if @column_type.nil?
if matched_column.type.to_s == @column_type.to_s
true
else
@missing = "#{model_class} has a db column named #{@column} " <<
"of type #{matched_column.type}, not #{@column_type}."
false
end
end
def correct_precision?
return true if @precision.nil?
if matched_column.precision.to_s == @precision.to_s
true
else
@missing = "#{model_class} has a db column named #{@column} " <<
"of precision #{matched_column.precision}, " <<
"not #{@precision}."
false
end
end
def correct_limit?
return true if @limit.nil?
if matched_column.limit.to_s == @limit.to_s
true
else
@missing = "#{model_class} has a db column named #{@column} " <<
"of limit #{matched_column.limit}, " <<
"not #{@limit}."
false
end
end
def correct_default?
return true if @default.nil?
if matched_column.default.to_s == @default.to_s
true
else
@missing = "#{model_class} has a db column named #{@column} " <<
"of default #{matched_column.default}, " <<
"not #{@default}."
false
end
end
def correct_null?
return true if @null.nil?
if matched_column.null.to_s == @null.to_s
true
else
@missing = "#{model_class} has a db column named #{@column} " <<
"of null #{matched_column.null}, " <<
"not #{@null}."
false
end
end
def correct_scale?
return true if @scale.nil?
if matched_column.scale.to_s == @scale.to_s
true
else
@missing = "#{model_class} has a db column named #{@column} " <<
"of scale #{matched_column.scale}, not #{@scale}."
false
end
end
def matched_column
model_class.columns.detect { |each| each.name == @column.to_s }
end
def model_class
@subject.class
end
def expectation
expected = "#{model_class.name} to #{description}"
end
end
end
end
end

View file

@ -0,0 +1,112 @@
module Shoulda # :nodoc:
module Matchers
module ActiveRecord # :nodoc:
# Ensures that there are DB indices on the given columns or tuples of
# columns.
#
# Options:
# * <tt>unique</tt> - whether or not the index has a unique
# constraint. Use <tt>true</tt> to explicitly test for a unique
# constraint. Use <tt>false</tt> to explicitly test for a non-unique
# constraint. Use <tt>nil</tt> if you don't care whether the index is
# unique or not. Default = <tt>nil</tt>
#
# Examples:
#
# it { should have_db_index(:age) }
# it { should have_db_index([:commentable_type, :commentable_id]) }
# it { should have_db_index(:ssn).unique(true) }
#
def have_db_index(columns)
HaveDbIndexMatcher.new(:have_index, columns)
end
class HaveDbIndexMatcher # :nodoc:
def initialize(macro, columns)
@macro = macro
@columns = normalize_columns_to_array(columns)
end
def unique(unique)
@unique = unique
self
end
def matches?(subject)
@subject = subject
index_exists? && correct_unique?
end
def failure_message
"Expected #{expectation} (#{@missing})"
end
def negative_failure_message
"Did not expect #{expectation}"
end
def description
"have a #{index_type} index on columns #{@columns.join(' and ')}"
end
protected
def index_exists?
! matched_index.nil?
end
def correct_unique?
return true if @unique.nil?
if matched_index.unique == @unique
true
else
@missing = "#{table_name} has an index named #{matched_index.name} " <<
"of unique #{matched_index.unique}, not #{@unique}."
false
end
end
def matched_index
indexes.detect { |each| each.columns == @columns }
end
def model_class
@subject.class
end
def table_name
model_class.table_name
end
def indexes
::ActiveRecord::Base.connection.indexes(table_name)
end
def expectation
expected = "#{model_class.name} to #{description}"
end
def index_type
case @unique
when nil
''
when false
'non-unique'
else
'unique'
end
end
def normalize_columns_to_array(columns)
if columns.class == Array
columns.collect { |each| each.to_s }
else
[columns.to_s]
end
end
end
end
end
end

View file

@ -0,0 +1,59 @@
module Shoulda # :nodoc:
module Matchers
module ActiveRecord # :nodoc:
# Ensures that the attribute cannot be changed once the record has been
# created.
#
# it { should have_readonly_attributes(:password) }
#
def have_readonly_attribute(value)
HaveReadonlyAttributeMatcher.new(value)
end
class HaveReadonlyAttributeMatcher # :nodoc:
def initialize(attribute)
@attribute = attribute.to_s
end
def matches?(subject)
@subject = subject
if readonly_attributes.include?(@attribute)
@negative_failure_message =
"Did not expect #{@attribute} to be read-only"
true
else
if readonly_attributes.empty?
@failure_message = "#{class_name} attribute #{@attribute} " <<
"is not read-only"
else
@failure_message = "#{class_name} is making " <<
"#{readonly_attributes.to_sentence} " <<
"read-only, but not #{@attribute}."
end
false
end
end
attr_reader :failure_message, :negative_failure_message
def description
"make #{@attribute} read-only"
end
private
def readonly_attributes
@readonly_attributes ||= (@subject.class.readonly_attributes || [])
end
def class_name
@subject.class.name
end
end
end
end
end

View file

@ -0,0 +1,34 @@
module Shoulda # :nodoc:
module Matchers
module ActiveRecord # :nodoc:
module Helpers
def pretty_error_messages(obj) # :nodoc:
obj.errors.map do |a, m|
msg = "#{a} #{m}"
msg << " (#{obj.send(a).inspect})" unless a.to_sym == :base
end
end
# Helper method that determines the default error message used by Active
# Record. Works for both existing Rails 2.1 and Rails 2.2 with the newly
# introduced I18n module used for localization.
#
# default_error_message(:blank)
# default_error_message(:too_short, :count => 5)
# default_error_message(:too_long, :count => 60)
def default_error_message(key, values = {})
if Object.const_defined?(:I18n) # Rails >= 2.2
result = I18n.translate("activerecord.errors.messages.#{key}", values)
if result =~ /^translation missing/
I18n.translate("errors.messages.#{key}", values)
else
result
end
else # Rails <= 2.1.x
::ActiveRecord::Errors.default_error_messages[key] % values[:count]
end
end
end
end
end
end

View file

@ -0,0 +1,41 @@
module Shoulda # :nodoc:
module Matchers
module ActiveRecord # :nodoc:
# Ensures that the model cannot be saved the given attribute is not
# accepted.
#
# Options:
# * <tt>with_message</tt> - value the test expects to find in
# <tt>errors.on(:attribute)</tt>. Regexp or string. Defaults to the
# translation for <tt>:accepted</tt>.
#
# Example:
# it { should validate_acceptance_of(:eula) }
#
def validate_acceptance_of(attr)
ValidateAcceptanceOfMatcher.new(attr)
end
class ValidateAcceptanceOfMatcher < ValidationMatcher # :nodoc:
def with_message(message)
@expected_message = message if message
self
end
def matches?(subject)
super(subject)
@expected_message ||= :accepted
disallows_value_of(false, @expected_message)
end
def description
"require #{@attribute} to be accepted"
end
end
end
end
end

View file

@ -0,0 +1,65 @@
module Shoulda # :nodoc:
module Matchers
module ActiveRecord # :nodoc:
# Ensures that the model is not valid if the given attribute is not
# formatted correctly.
#
# Options:
# * <tt>with_message</tt> - value the test expects to find in
# <tt>errors.on(:attribute)</tt>. <tt>Regexp</tt> or <tt>String</tt>.
# Defaults to the translation for <tt>:blank</tt>.
# * <tt>with(string to test against)</tt>
# * <tt>not_with(string to test against)</tt>
#
# Examples:
# it { should validate_format_of(:name).
# with('12345').
# with_message(/is not optional/) }
# it { should validate_format_of(:name).
# not_with('12D45').
# with_message(/is not optional/) }
#
def validate_format_of(attr)
ValidateFormatOfMatcher.new(attr)
end
class ValidateFormatOfMatcher < ValidationMatcher # :nodoc:
def initialize(attribute)
super
end
def with_message(message)
@expected_message = message if message
self
end
def with(value)
raise "You may not call both with and not_with" if @value_to_fail
@value_to_pass = value
self
end
def not_with(value)
raise "You may not call both with and not_with" if @value_to_pass
@value_to_fail = value
self
end
def matches?(subject)
super(subject)
@expected_message ||= :blank
return disallows_value_of(@value_to_fail, @expected_message) if @value_to_fail
allows_value_of(@value_to_pass, @expected_message) if @value_to_pass
end
def description
"#{@attribute} have a valid format"
end
end
end
end
end

View file

@ -0,0 +1,39 @@
module Shoulda # :nodoc:
module Matchers
module ActiveRecord # :nodoc:
# Ensure that the attribute is numeric
#
# Options:
# * <tt>with_message</tt> - value the test expects to find in
# <tt>errors.on(:attribute)</tt>. Regexp or string. Defaults to the
# translation for <tt>:not_a_number</tt>.
#
# Example:
# it { should validate_numericality_of(:age) }
#
def validate_numericality_of(attr)
ValidateNumericalityOfMatcher.new(attr)
end
class ValidateNumericalityOfMatcher < ValidationMatcher # :nodoc:
def with_message(message)
@expected_message = message if message
self
end
def matches?(subject)
super(subject)
@expected_message ||= :not_a_number
disallows_value_of('abcd', @expected_message)
end
def description
"only allow numeric values for #{@attribute}"
end
end
end
end
end

View file

@ -0,0 +1,60 @@
module Shoulda # :nodoc:
module Matchers
module ActiveRecord # :nodoc:
# Ensures that the model is not valid if the given attribute is not
# present.
#
# Options:
# * <tt>with_message</tt> - value the test expects to find in
# <tt>errors.on(:attribute)</tt>. <tt>Regexp</tt> or <tt>String</tt>.
# Defaults to the translation for <tt>:blank</tt>.
#
# Examples:
# it { should validate_presence_of(:name) }
# it { should validate_presence_of(:name).
# with_message(/is not optional/) }
#
def validate_presence_of(attr)
ValidatePresenceOfMatcher.new(attr)
end
class ValidatePresenceOfMatcher < ValidationMatcher # :nodoc:
def with_message(message)
@expected_message = message if message
self
end
def matches?(subject)
super(subject)
@expected_message ||= :blank
disallows_value_of(blank_value, @expected_message)
end
def description
"require #{@attribute} to be set"
end
private
def blank_value
if collection?
[]
else
nil
end
end
def collection?
if reflection = @subject.class.reflect_on_association(@attribute)
[:has_many, :has_and_belongs_to_many].include?(reflection.macro)
else
false
end
end
end
end
end
end

View file

@ -0,0 +1,148 @@
module Shoulda # :nodoc:
module Matchers
module ActiveRecord # :nodoc:
# Ensures that the model is invalid if the given attribute is not unique.
#
# Internally, this uses values from existing records to test validations,
# so this will always fail if you have not saved at least one record for
# the model being tested, like so:
#
# describe User do
# before(:each) { User.create!(:email => 'address@example.com') }
# it { should validate_uniqueness_of(:email) }
# end
#
# Options:
#
# * <tt>with_message</tt> - value the test expects to find in
# <tt>errors.on(:attribute)</tt>. <tt>Regexp</tt> or <tt>String</tt>.
# Defaults to the translation for <tt>:taken</tt>.
# * <tt>scoped_to</tt> - field(s) to scope the uniqueness to.
# * <tt>case_insensitive</tt> - ensures that the validation does not
# check case. Off by default. Ignored by non-text attributes.
#
# Examples:
# it { should validate_uniqueness_of(:keyword) }
# it { should validate_uniqueness_of(:keyword).with_message(/dup/) }
# it { should validate_uniqueness_of(:email).scoped_to(:name) }
# it { should validate_uniqueness_of(:email).
# scoped_to(:first_name, :last_name) }
# it { should validate_uniqueness_of(:keyword).case_insensitive }
#
def validate_uniqueness_of(attr)
ValidateUniquenessOfMatcher.new(attr)
end
class ValidateUniquenessOfMatcher < ValidationMatcher # :nodoc:
include Helpers
def initialize(attribute)
@attribute = attribute
end
def scoped_to(*scopes)
@scopes = [*scopes].flatten
self
end
def with_message(message)
@expected_message = message
self
end
def case_insensitive
@case_insensitive = true
self
end
def description
result = "require "
result << "case sensitive " unless @case_insensitive
result << "unique value for #{@attribute}"
result << " scoped to #{@scopes.join(', ')}" unless @scopes.blank?
result
end
def matches?(subject)
@subject = subject.class.new
@expected_message ||= :taken
find_existing &&
set_scoped_attributes &&
validate_attribute &&
validate_after_scope_change
end
private
def find_existing
if @existing = @subject.class.find(:first)
true
else
@failure_message = "Can't find first #{class_name}"
false
end
end
def set_scoped_attributes
unless @scopes.blank?
@scopes.each do |scope|
setter = :"#{scope}="
unless @subject.respond_to?(setter)
@failure_message =
"#{class_name} doesn't seem to have a #{scope} attribute."
return false
end
@subject.send("#{scope}=", @existing.send(scope))
end
end
true
end
def validate_attribute
disallows_value_of(existing_value, @expected_message)
end
# TODO: There is a chance that we could change the scoped field
# to a value that's already taken. An alternative implementation
# could actually find all values for scope and create a unique
def validate_after_scope_change
if @scopes.blank?
true
else
@scopes.all? do |scope|
previous_value = @existing.send(scope)
# Assume the scope is a foreign key if the field is nil
previous_value ||= 0
next_value = previous_value.next
@subject.send("#{scope}=", next_value)
if allows_value_of(existing_value, @expected_message)
@negative_failure_message <<
" (with different value of #{scope})"
true
else
@failure_message << " (with different value of #{scope})"
false
end
end
end
end
def class_name
@subject.class.name
end
def existing_value
value = @existing.send(@attribute)
value.swapcase! if @case_insensitive && value.respond_to?(:swapcase!)
value
end
end
end
end
end

View file

@ -0,0 +1,56 @@
module Shoulda # :nodoc:
module Matchers
module ActiveRecord # :nodoc:
class ValidationMatcher # :nodoc:
attr_reader :failure_message
def initialize(attribute)
@attribute = attribute
end
def negative_failure_message
@negative_failure_message || @failure_message
end
def matches?(subject)
@subject = subject
false
end
private
def allows_value_of(value, message = nil)
allow = AllowValueMatcher.
new(value).
for(@attribute).
with_message(message)
if allow.matches?(@subject)
@negative_failure_message = allow.failure_message
true
else
@failure_message = allow.negative_failure_message
false
end
end
def disallows_value_of(value, message = nil)
disallow = AllowValueMatcher.
new(value).
for(@attribute).
with_message(message)
if disallow.matches?(@subject)
@failure_message = disallow.negative_failure_message
false
else
@negative_failure_message = disallow.failure_message
true
end
end
end
end
end
end

View file

@ -0,0 +1,23 @@
# :enddoc:
if defined?(::ActiveRecord)
require 'shoulda/matchers/active_record'
module RSpec::Matchers
include Shoulda::Matchers::ActiveRecord
end
end
if defined?(::ActionController)
require 'shoulda/matchers/action_controller'
module RSpec::Rails::ControllerExampleGroup
include Shoulda::Matchers::ActionController
end
end
if defined?(::ActionMailer)
require 'shoulda/matchers/action_mailer'
module RSpec::Rails::MailerExampleGroup
include Shoulda::Matchers::ActionMailer
end
end

View file

@ -0,0 +1,41 @@
# :enddoc:
if defined?(ActionController)
require 'shoulda/matchers/action_controller'
class ActionController::TestCase
include Shoulda::Matchers::ActionController
extend Shoulda::Matchers::ActionController
def subject
@controller
end
end
end
if defined?(ActionMailer)
require 'shoulda/matchers/action_mailer'
module Test
module Unit
class TestCase
include Shoulda::Matchers::ActionMailer
extend Shoulda::Matchers::ActionMailer
end
end
end
end
if defined?(ActiveRecord)
require 'shoulda/matchers/active_record'
module Test
module Unit
class TestCase
include Shoulda::Matchers::ActiveRecord
extend Shoulda::Matchers::ActiveRecord
end
end
end
end

View file

@ -0,0 +1,6 @@
module Shoulda
module Matchers
VERSION = "2.11.3"
end
end

View file

@ -1,4 +0,0 @@
module Shoulda
VERSION = "2.11.3"
end

View file

@ -1,9 +1,9 @@
$LOAD_PATH << File.join(File.dirname(__FILE__), 'lib')
require 'shoulda/version'
require 'shoulda/matchers/version'
Gem::Specification.new do |s|
s.name = %q{shoulda}
s.version = Shoulda::VERSION
s.name = %q{shoulda-matchers}
s.version = Shoulda::Matchers::VERSION
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
s.authors = ["Tammer Saleh", "Joe Ferris", "Ryan McGeary", "Dan Croak",
@ -15,7 +15,6 @@ Gem::Specification.new do |s|
s.homepage = %q{http://thoughtbot.com/community/}
s.rdoc_options = ["--line-numbers", "--main", "README.rdoc"]
s.require_paths = ["lib"]
s.rubyforge_project = %q{shoulda}
s.rubygems_version = %q{1.3.5}
s.summary = %q{Making tests easy on the fingers and eyes}
s.description = %q{Making tests easy on the fingers and eyes}

View file

@ -1,6 +1,6 @@
require 'spec_helper'
describe Shoulda::ActionController::AssignToMatcher do
describe Shoulda::Matchers::ActionController::AssignToMatcher do
context "a controller that assigns to an instance variable" do
before do

View file

@ -1,6 +1,6 @@
require 'spec_helper'
describe Shoulda::ActionController::FilterParamMatcher do
describe Shoulda::Matchers::ActionController::FilterParamMatcher do
context "given parameter filters" do
before do

View file

@ -1,6 +1,6 @@
require 'spec_helper'
describe Shoulda::ActionController::RedirectToMatcher do
describe Shoulda::Matchers::ActionController::RedirectToMatcher do
context "a controller that redirects" do
before do

View file

@ -1,6 +1,6 @@
require 'spec_helper'
describe Shoulda::ActionController::RenderTemplateMatcher do
describe Shoulda::Matchers::ActionController::RenderTemplateMatcher do
include ActionController::TemplateAssertions
context "a controller that renders a template" do

View file

@ -1,6 +1,6 @@
require 'spec_helper'
describe Shoulda::ActionController::RenderWithLayoutMatcher do
describe Shoulda::Matchers::ActionController::RenderWithLayoutMatcher do
include ActionController::TemplateAssertions
context "a controller that renders with a layout" do

View file

@ -1,6 +1,6 @@
require 'spec_helper'
describe Shoulda::ActionController::RespondWithContentTypeMatcher do
describe Shoulda::Matchers::ActionController::RespondWithContentTypeMatcher do
context "a controller responding with content type :xml" do
before do

View file

@ -1,6 +1,6 @@
require 'spec_helper'
describe Shoulda::ActionController::RespondWithMatcher do
describe Shoulda::Matchers::ActionController::RespondWithMatcher do
context "a controller responding with success" do
before do

View file

@ -1,6 +1,6 @@
require 'spec_helper'
describe Shoulda::ActionController::RouteMatcher do
describe Shoulda::Matchers::ActionController::RouteMatcher do
context "given a controller with a defined glob url" do
before do

View file

@ -1,6 +1,6 @@
require 'spec_helper'
describe Shoulda::ActionController::SetSessionMatcher do
describe Shoulda::Matchers::ActionController::SetSessionMatcher do
context "a controller that sets a session variable" do
before do

View file

@ -1,6 +1,6 @@
require 'spec_helper'
describe Shoulda::ActionController::SetTheFlashMatcher do
describe Shoulda::Matchers::ActionController::SetTheFlashMatcher do
context "a controller that sets a flash message" do
before do

View file

@ -1,6 +1,6 @@
require 'spec_helper'
describe Shoulda::ActionMailer::HaveSentEmailMatcher do
describe Shoulda::Matchers::ActionMailer::HaveSentEmailMatcher do
def add_mail_to_deliveries
::ActionMailer::Base.deliveries << Mailer.the_email
end

View file

@ -1,6 +1,6 @@
require 'spec_helper'
describe Shoulda::ActiveRecord::AllowMassAssignmentOfMatcher do
describe Shoulda::Matchers::ActiveRecord::AllowMassAssignmentOfMatcher do
context "an attribute that is blacklisted from mass-assignment" do
before do

View file

@ -1,6 +1,6 @@
require 'spec_helper'
describe Shoulda::ActiveRecord::AllowValueMatcher do
describe Shoulda::Matchers::ActiveRecord::AllowValueMatcher do
context "an attribute with a format validation" do
before do

View file

@ -1,6 +1,6 @@
require 'spec_helper'
describe Shoulda::ActiveRecord::AssociationMatcher do
describe Shoulda::Matchers::ActiveRecord::AssociationMatcher do
context "belong_to" do
before do

View file

@ -1,6 +1,6 @@
require 'spec_helper'
describe Shoulda::ActiveRecord::EnsureInclusionOfMatcher do
describe Shoulda::Matchers::ActiveRecord::EnsureInclusionOfMatcher do
context "an attribute which must be included in a range" do
before do

View file

@ -1,6 +1,6 @@
require 'spec_helper'
describe Shoulda::ActiveRecord::EnsureLengthOfMatcher do
describe Shoulda::Matchers::ActiveRecord::EnsureLengthOfMatcher do
context "an attribute with a non-zero minimum length validation" do
before do

View file

@ -1,6 +1,6 @@
require 'spec_helper'
describe Shoulda::ActiveRecord::HaveDbColumnMatcher do
describe Shoulda::Matchers::ActiveRecord::HaveDbColumnMatcher do
context "have_db_column" do
before do

View file

@ -1,6 +1,6 @@
require 'spec_helper'
describe Shoulda::ActiveRecord::HaveDbIndexMatcher do
describe Shoulda::Matchers::ActiveRecord::HaveDbIndexMatcher do
context "have_db_index" do
before do

View file

@ -1,6 +1,6 @@
require 'spec_helper'
describe Shoulda::ActiveRecord::HaveReadonlyAttributeMatcher do
describe Shoulda::Matchers::ActiveRecord::HaveReadonlyAttributeMatcher do
context "an attribute that cannot be set after being saved" do
before do

View file

@ -1,6 +1,6 @@
require 'spec_helper'
describe Shoulda::ActiveRecord::ValidateAcceptanceOfMatcher do
describe Shoulda::Matchers::ActiveRecord::ValidateAcceptanceOfMatcher do
context "an attribute which must be accepted" do
before do

View file

@ -1,6 +1,6 @@
require 'spec_helper'
describe Shoulda::ActiveRecord::ValidateFormatOfMatcher do
describe Shoulda::Matchers::ActiveRecord::ValidateFormatOfMatcher do
context "a postal code" do
before do

View file

@ -1,6 +1,6 @@
require 'spec_helper'
describe Shoulda::ActiveRecord::ValidateNumericalityOfMatcher do
describe Shoulda::Matchers::ActiveRecord::ValidateNumericalityOfMatcher do
context "a numeric attribute" do
before do

View file

@ -1,6 +1,6 @@
require 'spec_helper'
describe Shoulda::ActiveRecord::ValidatePresenceOfMatcher do
describe Shoulda::Matchers::ActiveRecord::ValidatePresenceOfMatcher do
context "a required attribute" do
before do

View file

@ -1,6 +1,6 @@
require 'spec_helper'
describe Shoulda::ActiveRecord::ValidateUniquenessOfMatcher do
describe Shoulda::Matchers::ActiveRecord::ValidateUniquenessOfMatcher do
context "a unique attribute" do
before do

View file

@ -15,7 +15,7 @@ $LOAD_PATH << File.join(PROJECT_ROOT, 'lib')
Dir[File.join(PROJECT_ROOT, 'spec', 'support', '**', '*.rb')].each { |file| require(file) }
require 'shoulda'
require 'shoulda-matchers'
require 'rspec/rails'
# Run the migrations
@ -24,9 +24,9 @@ ActiveRecord::Migrator.migrate("#{Rails.root}/db/migrate")
RSpec.configure do |config|
config.mock_with :mocha
config.include Shoulda::ActionController,
config.include Shoulda::Matchers::ActionController,
:example_group => { :file_path => /action_controller/ }
config.include Shoulda::ActionMailer,
config.include Shoulda::Matchers::ActionMailer,
:example_group => { :file_path => /action_mailer/ }
end