diff --git a/README.md b/README.md index dc48cc27..efb60b8a 100644 --- a/README.md +++ b/README.md @@ -1314,6 +1314,29 @@ class UserControllerTest < ActionController::TestCase end ``` +## Independent Matchers + +Matchers to test non-Rails-dependent code: + +#### delegate_method + +```ruby +class Human < ActiveRecord::Base + has_one :robot + delegate :work, to: :robot +end + +# RSpec +describe Human do + it { should delegate_method(:work).to(:robot) } +end + +# Test::Unit +class HumanTest < ActiveSupport::TestCase + should delegate_method(:work).to(:robot) +end +``` + ## Versioning shoulda-matchers follows Semantic Versioning 2.0 as defined at diff --git a/lib/shoulda/matchers/independent.rb b/lib/shoulda/matchers/independent.rb new file mode 100644 index 00000000..e8119a10 --- /dev/null +++ b/lib/shoulda/matchers/independent.rb @@ -0,0 +1,9 @@ +require 'shoulda/matchers/independent/delegate_matcher' + +module Shoulda + module Matchers + # = Matchers for non-Rails-dependent code. + module Independent + end + end +end diff --git a/lib/shoulda/matchers/independent/delegate_matcher.rb b/lib/shoulda/matchers/independent/delegate_matcher.rb new file mode 100644 index 00000000..bcf0caa5 --- /dev/null +++ b/lib/shoulda/matchers/independent/delegate_matcher.rb @@ -0,0 +1,132 @@ +require 'active_support/deprecation' + +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: + # * :as - tests that the object being delegated to is called with a certain method + # (defaults to same name as delegating method) + # * :with_arguments - 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.', :hastily => true) } + # + def delegate_method(delegating_method) + DelegateMatcher.new(delegating_method) + 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) + + matcher = Mocha::API::HaveReceived.new(method_on_target).with(*@delegated_arguments) + matcher.matches?(stubbed_object) + rescue NoMethodError, MiniTest::Assertion + false + end + end + + def description + add_clarifications_to("delegate method ##{@delegating_method} to :#{@target_method}") + 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_for_should + base = "Expected #{delegating_method_name} to delegate to #{target_method_name}" + add_clarifications_to(base) + end + + private + + def add_clarifications_to(message) + if @delegated_arguments.present? + message << " with arguments: #{@delegated_arguments.inspect}" + end + + if @method_on_target.present? + message << " as ##{@method_on_target}" + end + + message + 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! + if @target_method.blank? + raise TargetNotDefinedError + end + 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 diff --git a/lib/shoulda/matchers/integrations/rspec.rb b/lib/shoulda/matchers/integrations/rspec.rb index 7ae1a3a3..455107f0 100644 --- a/lib/shoulda/matchers/integrations/rspec.rb +++ b/lib/shoulda/matchers/integrations/rspec.rb @@ -2,6 +2,9 @@ require 'rspec/core' RSpec.configure do |config| + require 'shoulda/matchers/independent' + config.include Shoulda::Matchers::Independent + if defined?(::ActiveRecord) require 'shoulda/matchers/active_record' require 'shoulda/matchers/active_model' diff --git a/lib/shoulda/matchers/integrations/test_unit.rb b/lib/shoulda/matchers/integrations/test_unit.rb index dca1094a..f06ef90b 100644 --- a/lib/shoulda/matchers/integrations/test_unit.rb +++ b/lib/shoulda/matchers/integrations/test_unit.rb @@ -1,3 +1,16 @@ +# :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) && defined?(ActionController::TestCase) require 'shoulda/matchers/action_controller' diff --git a/spec/shoulda/matchers/independent/delegate_matcher_spec.rb b/spec/shoulda/matchers/independent/delegate_matcher_spec.rb new file mode 100644 index 00000000..47fb427c --- /dev/null +++ b/spec/shoulda/matchers/independent/delegate_matcher_spec.rb @@ -0,0 +1,204 @@ +require 'spec_helper' + +describe Shoulda::Matchers::Independent::DelegateMatcher do + context '#description' do + context 'by default' do + it 'states that it should delegate method to the right object' do + matcher = delegate_method(:method_name).to(:target) + + matcher.description.should == 'delegate method #method_name to :target' + end + end + + context 'with #as chain' do + it 'states that it should delegate method to the right object and method' do + matcher = delegate_method(:method_name).to(:target).as(:alternate) + + matcher.description.should == 'delegate method #method_name to :target as #alternate' + end + end + + context 'with #with_argument chain' do + it 'states that it should delegate method to the right object with right argument' do + matcher = delegate_method(:method_name).to(:target).with_arguments(:foo, :bar => [1, 2]) + + matcher.description.should == + 'delegate method #method_name to :target with arguments: [:foo, {:bar=>[1, 2]}]' + end + end + end + + 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 'raises an error if no delegation target is defined' do + expect { Object.new.should delegate_method(:name) }. + to raise_exception described_class::TargetNotDefinedError + end + + it 'raises an error if called with #should_not' do + expect { Object.new.should_not delegate_method(:name).to(:anyone) }. + to raise_exception described_class::InvalidDelegateMatcher + end + + context 'given a method that does not delegate' do + before do + define_class(:post_office) do + def deliver_mail + :delivered + end + end + end + + it 'rejects' do + post_office = PostOffice.new + matcher = delegate_method(:deliver_mail).to(:mailman) + + matcher.matches?(post_office).should be_false + end + + it 'has a failure message that indicates which method should have been delegated' do + post_office = PostOffice.new + matcher = delegate_method(:deliver_mail).to(:mailman) + + matcher.matches?(post_office) + + message = 'Expected PostOffice#deliver_mail to delegate to PostOffice#mailman' + matcher.failure_message_for_should.should == message + end + + it 'uses the proper syntax for class methods in errors' do + matcher = delegate_method(:deliver_mail).to(:mailman) + + matcher.matches?(PostOffice) + + message = 'Expected PostOffice.deliver_mail to delegate to PostOffice.mailman' + matcher.failure_message_for_should.should == message + end + end + + context 'given a method that delegates properly' do + it 'accepts' do + define_class(:mailman) + define_class(:post_office) do + def deliver_mail + mailman.deliver_mail + end + + def mailman + Mailman.new + end + end.new.should delegate_method(:deliver_mail).to(:mailman) + end + end + + context 'given a method that delegates properly with certain arguments' do + before do + define_class(:mailman) + define_class(:post_office) do + def deliver_mail + mailman.deliver_mail('221B Baker St.', :hastily => true) + end + + def mailman + Mailman.new + end + end + end + + context 'when given the correct arguments' do + it 'accepts' do + PostOffice.new.should delegate_method(:deliver_mail). + to(:mailman).with_arguments('221B Baker St.', :hastily => true) + end + end + + context 'when not given the correct arguments' do + it 'rejects' do + post_office = PostOffice.new + matcher = delegate_method(:deliver_mail).to(:mailman). + with_arguments('123 Nowhere Ln.') + matcher.matches?(post_office).should be_false + end + + it 'has a failure message that indicates which arguments were expected' do + post_office = PostOffice.new + matcher = delegate_method(:deliver_mail).to(:mailman).with_arguments('123 Nowhere Ln.') + + matcher.matches?(post_office) + + message = 'Expected PostOffice#deliver_mail to delegate to PostOffice#mailman with arguments: ["123 Nowhere Ln."]' + matcher.failure_message_for_should.should == message + end + end + end + + context 'given a method that delegates properly to a method of a different name' do + before do + define_class(:mailman) + define_class(:post_office) do + 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 'accepts' do + PostOffice.new. + should delegate_method(:deliver_mail).to(:mailman).as(:deliver_mail_and_avoid_dogs) + end + end + + context 'when given an incorrect method name' do + it 'rejects' do + post_office = PostOffice.new + matcher = delegate_method(:deliver_mail).to(:mailman).as(:watch_tv) + matcher.matches?(post_office).should be_false + end + + it 'has a failure message that indicates which method was expected' do + post_office = PostOffice.new + matcher = delegate_method(:deliver_mail).to(:mailman).as(:watch_tv) + + matcher.matches?(post_office) + + message = 'Expected PostOffice#deliver_mail to delegate to PostOffice#mailman as #watch_tv' + matcher.failure_message_for_should.should == message + 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