Permalink
Browse files

Add #delegate_method matcher.

  • Loading branch information...
1 parent c8e3f09 commit 2377fb44aff4da005a3fce6fde43c60bc975149b @readeharris readeharris committed with drapergeek Oct 8, 2012
@@ -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
@@ -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
@@ -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'
@@ -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'
@@ -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.