Skip to content

Commit

Permalink
Add #delegate_method matcher.
Browse files Browse the repository at this point in the history
  • Loading branch information
readeharris authored and drapergeek committed Oct 19, 2012
1 parent c8e3f09 commit 2377fb4
Show file tree
Hide file tree
Showing 5 changed files with 315 additions and 0 deletions.
9 changes: 9 additions & 0 deletions lib/shoulda/matchers/independent.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
require 'shoulda/matchers/independent/delegate_matcher'

module Shoulda
module Matchers
# = Matchers for plain ol' Ruby objects.
module Independent
end
end
end
123 changes: 123 additions & 0 deletions lib/shoulda/matchers/independent/delegate_matcher.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
module Shoulda # :nodoc:
module Matchers
module Independent # :nodoc:

# Ensure that a given method is delegated properly.
#
# Basic Syntax:
# it { should delegate_method(:deliver_mail).to(:mailman) }
#
# Options:
# * <tt>:as</tt> - tests that the object being delegated to is called with a certain method (defaults to same name as delegating method)
# * <tt>:with_arguments</tt> - tests that the method on the object being delegated to is called with certain arguments
#
# Examples:
# it { should delegate_method(:deliver_mail).to(:mailman).as(:deliver_with_haste)
# it { should delegate_method(:deliver_mail).to(:mailman).with_arguments("221B Baker St.", speed: :presently)
#
def delegate_method(delegating_method)
require 'bourne'
DelegateMatcher.new(delegating_method)
rescue LoadError
raise "To use Shoulda's #delegate_method matcher, please add `bourne` to your Gemfile."
end

class DelegateMatcher
def initialize(delegating_method)
@delegating_method = delegating_method
end

def matches?(subject)
@subject = subject
ensure_target_method_is_present!

begin
extend Mocha::API

stubbed_object = stub(method_on_target)
subject.stubs(@target_method).returns(stubbed_object)
subject.send(@delegating_method)

stubbed_object.should have_received(method_on_target).with(*@delegated_arguments)
rescue NoMethodError, RSpec::Expectations::ExpectationNotMetError, Mocha::ExpectationError
false
end
end

def does_not_match?(subject)
raise InvalidDelegateMatcher
end

def to(target_method)
@target_method = target_method
self
end

def as(method_on_target)
@method_on_target = method_on_target
self
end

def with_arguments(*arguments)
@delegated_arguments = arguments
self
end

def failure_message
base = "Expected #{delegating_method_name} to delegate to #{target_method_name}"
add_clarifications_to(base)
end

private

def add_clarifications_to(message)
message.tap do |message|
if @delegated_arguments.present?
message.concat(" with arguments: #{@delegated_arguments}")
end

if @method_on_target.present?
message.concat(" as :#{@method_on_target}")
end
end
end

def delegating_method_name
method_name_with_class(@delegating_method)
end

def target_method_name
method_name_with_class(@target_method)
end

def method_name_with_class(method)
if Class === @subject
@subject.name + "." + method.to_s
else
@subject.class.name + "#" + method.to_s
end
end

def method_on_target
@method_on_target || @delegating_method
end

def ensure_target_method_is_present!
raise TargetNotDefinedError unless @target_method.present?
end
end

class DelegateMatcher::TargetNotDefinedError < StandardError
def message
"Delegation needs a target. Use the #to method to define one, e.g. `post_office.should delegate(:deliver_mail).to(:mailman)`"
end
end

class DelegateMatcher::InvalidDelegateMatcher < StandardError
def message
"#delegate_to does not support #should_not syntax."
end
end
end
end
end
5 changes: 5 additions & 0 deletions lib/shoulda/matchers/integrations/rspec.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# :enddoc:

require 'shoulda/matchers/independent'
module RSpec::Matchers
include Shoulda::Matchers::Independent
end

if defined?(::ActiveRecord)
require 'shoulda/matchers/active_record'
require 'shoulda/matchers/active_model'
Expand Down
11 changes: 11 additions & 0 deletions lib/shoulda/matchers/integrations/test_unit.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
# :enddoc:
require 'test/unit/testcase'

require 'shoulda/matchers/independent'
module Test
module Unit
class TestCase
include Shoulda::Matchers::Independent
extend Shoulda::Matchers::Independent
end
end
end

if defined?(ActionController)
require 'shoulda/matchers/action_controller'
Expand Down
167 changes: 167 additions & 0 deletions spec/shoulda/independent/delegate_matcher_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
require 'spec_helper'

describe Shoulda::Matchers::Independent::DelegateMatcher do
it 'supports chaining on #to' do
matcher = delegate_method(:method)
matcher.to(:another_method).should == matcher
end

it 'supports chaining on #with_arguments' do
matcher = delegate_method(:method)
matcher.with_arguments(1, 2, 3).should == matcher
end

it 'supports chaining on #as' do
matcher = delegate_method(:method)
matcher.as(:some_other_method).should == matcher
end

it 'should raise an error if no delegation target is defined' do
object = Object.new
expect {
object.should delegate_method(:name)
}.to raise_exception Shoulda::Matchers::Independent::DelegateMatcher::TargetNotDefinedError
end

it 'should raise an error if called with #should_not' do
object = Object.new
expect {
object.should_not delegate_method(:name).to(:anyone)
}.to raise_exception Shoulda::Matchers::Independent::DelegateMatcher::InvalidDelegateMatcher
end

context 'given a method that does not delegate' do
before do
class PostOffice
def deliver_mail
:delivered
end
end
end

it 'fails with a useful message' do
begin
post_office = PostOffice.new
post_office.should delegate_method(:deliver_mail).to(:mailman)
rescue Exception => e
e.message.should == 'Expected PostOffice#deliver_mail to delegate to PostOffice#mailman'
end
end

it 'uses the proper syntax for class methods in errors' do
begin
PostOffice.should delegate_method(:deliver_mail).to(:mailman)
rescue => e
e.message.should == 'Expected PostOffice.deliver_mail to delegate to PostOffice.mailman'
end
end
end

context 'given a method that delegates properly' do
before do
class Mailman; end
class PostOffice
def deliver_mail
mailman.deliver_mail
end

def mailman
Mailman.new
end
end
end

it 'succeeds' do
post_office = PostOffice.new
post_office.should delegate_method(:deliver_mail).to(:mailman)
end
end

context 'given a method that delegates properly with certain arguments' do
before do
class Mailman; end
class PostOffice
def deliver_mail
mailman.deliver_mail("221B Baker St.", speed: :presently)
end

def mailman
Mailman.new
end
end
end

context 'when given the correct arguments' do
it 'succeeds' do
post_office = PostOffice.new
post_office.should delegate_method(:deliver_mail)
.to(:mailman)
.with_arguments("221B Baker St.", speed: :presently)
end
end

context 'when not given the correct arguments' do
it 'fails with a useful message' do
begin
post_office = PostOffice.new
post_office.should delegate_method(:deliver_mail)
.to(:mailman)
.with_arguments("123 Nowhere Ln.")
rescue Exception => e
e.message.should == 'Expected PostOffice#deliver_mail to delegate to PostOffice#mailman with arguments: ["123 Nowhere Ln."]'
end
end
end
end

context 'given a method that delegates properly to a method of a different name' do
before do
class Mailman; end
class PostOffice
def deliver_mail
mailman.deliver_mail_and_avoid_dogs
end

def mailman
Mailman.new
end
end
end

context 'when given the correct method name' do
it 'succeeds' do
post_office = PostOffice.new
post_office.should delegate_method(:deliver_mail)
.to(:mailman)
.as(:deliver_mail_and_avoid_dogs)
end
end

context 'when given an incorrect method name' do
it 'fails with a useful message' do
begin
post_office = PostOffice.new
post_office.should delegate_method(:deliver_mail)
.to(:mailman)
.as(:deliver_mail_without_regard_for_safety)
rescue Exception => e
e.message.should == "Expected PostOffice#deliver_mail to delegate to PostOffice#mailman as :deliver_mail_without_regard_for_safety"
end
end
end
end
end

describe Shoulda::Matchers::Independent::DelegateMatcher::TargetNotDefinedError do
it 'has a useful message' do
error = Shoulda::Matchers::Independent::DelegateMatcher::TargetNotDefinedError.new
error.message.should include "Delegation needs a target."
end
end

describe Shoulda::Matchers::Independent::DelegateMatcher::InvalidDelegateMatcher do
it 'has a useful message' do
error = Shoulda::Matchers::Independent::DelegateMatcher::InvalidDelegateMatcher.new
error.message.should include "does not support #should_not"
end
end

0 comments on commit 2377fb4

Please sign in to comment.