Permalink
Browse files

Fix respond_to matcher.

- Object.new.should_not respond_to(:object_id, :some_undefined_method)
  should fail (but passed before).
- Refactored and DRYed up the matcher a bit.
- Added argument arity info to failure_message_for_should_not.
- Added test coverage for more should_not cases.
- Closes #26.
  • Loading branch information...
1 parent b1a6c84 commit 64f9e9ab952783951599362f5258d8f19f4fcb27 @myronmarston myronmarston committed with dchelimsky Oct 7, 2010
@@ -0,0 +1,79 @@
+Feature: respond_to matcher
+
+ rspec-expectations includes a matcher to assist in checking an object's
+ interface. In its most basic form:
+
+ obj.should respond_to(:foo) # pass if obj.respond_to?(:foo)
+
+ You can perform multiple #respond_to? checks with a single matcher, if
+ you pass multiple arguments:
+
+ obj.should respond_to(:foo, :bar) # passes if obj.respond_to?(:foo) && obj.respond_to?(:bar)
+
+ If the number of arguments accepted by the method is important to you,
+ you can check that as well:
+
+ obj.should respond_to(:foo).with(1).argument
+ obj.should respond_to(:bar).with(2).arguments
+
+ Note that this matcher relies entirely upon #respond_to?. If an object
+ dynamically responds to a message via #method_missing, but does not
+ indicate this via #respond_to?, then this matcher will give you
+ false results.
+
+ Scenario: basic usage
+ Given a file named "respond_to_matcher_spec.rb" with:
+ """
+ describe "a string" do
+ it { should respond_to(:length) }
+ it { should respond_to(:hash, :class, :to_s) }
+ it { should_not respond_to(:to_model) }
+ it { should_not respond_to(:compact, :flatten) }
+
+ # deliberate failures
+ it { should respond_to(:to_model) }
+ it { should respond_to(:compact, :flatten) }
+ it { should_not respond_to(:length) }
+ it { should_not respond_to(:hash, :class, :to_s) }
+
+ # mixed examples--String responds to :length but not :flatten
+ # both specs should fail
+ it { should respond_to(:length, :flatten) }
+ it { should_not respond_to(:length, :flatten) }
+ end
+ """
+ When I run "rspec respond_to_matcher_spec.rb"
+ Then the output should contain all of these:
+ | 10 examples, 6 failures |
+ | expected "a string" to respond to :to_model |
+ | expected "a string" to respond to :compact, :flatten |
+ | expected "a string" not to respond to :length |
+ | expected "a string" not to respond to :hash, :class, :to_s |
+ | expected "a string" to respond to :flatten |
+ | expected "a string" not to respond to :length |
+
+ Scenario: argument checking
+ Given a file named "respond_to_matcher_argument_checking_spec.rb" with:
+ """
+ describe 7 do
+ it { should respond_to(:zero?).with(0).arguments }
+ it { should_not respond_to(:zero?).with(1).argument }
+
+ it { should respond_to(:between?).with(2).arguments }
+ it { should_not respond_to(:between?).with(7).arguments }
+
+ # deliberate failures
+ it { should respond_to(:zero?).with(1).argument }
+ it { should_not respond_to(:zero?).with(0).arguments }
+
+ it { should respond_to(:between?).with(7).arguments }
+ it { should_not respond_to(:between?).with(2).arguments }
+ end
+ """
+ When I run "rspec respond_to_matcher_argument_checking_spec.rb"
+ Then the output should contain all of these:
+ | 8 examples, 4 failures |
+ | expected 7 to respond to :zero? with 1 argument |
+ | expected 7 not to respond to :zero? with 0 arguments |
+ | expected 7 to respond to :between? with 7 arguments |
+ | expected 7 not to respond to :between? with 2 arguments |
@@ -5,3 +5,9 @@
end
end
+Then /^the output should contain all of these:$/ do |table|
+ table.raw.flatten.each do |string|
+ assert_partial_output(string)
+ end
+end
+
@@ -5,23 +5,22 @@ class RespondTo #:nodoc:
def initialize(*names)
@names = names
@expected_arity = nil
- @names_not_responded_to = []
end
def matches?(actual)
- @actual = actual
- @names.each do |name|
- @names_not_responded_to << name unless actual.respond_to?(name) && matches_arity?(actual, name)
- end
- return @names_not_responded_to.empty?
+ find_failing_method_names(actual, :reject).empty?
+ end
+
+ def does_not_match?(actual)
+ find_failing_method_names(actual, :select).empty?
end
def failure_message_for_should
- "expected #{@actual.inspect} to respond to #{@names_not_responded_to.collect {|name| name.inspect }.join(', ')}#{with_arity}"
+ "expected #{@actual.inspect} to respond to #{@failing_method_names.collect {|name| name.inspect }.join(', ')}#{with_arity}"
end
def failure_message_for_should_not
- "expected #{@actual.inspect} not to respond to #{@names.collect {|name| name.inspect }.join(', ')}"
+ failure_message_for_should.sub(/to respond to/, 'not to respond to')
end
def description
@@ -39,9 +38,24 @@ def argument
alias :arguments :argument
private
+
+ def find_failing_method_names(actual, filter_method)
+ @actual = actual
+ @failing_method_names = @names.send(filter_method) do |name|
+ @actual.respond_to?(name) && matches_arity?(actual, name)
+ end
+ end
def matches_arity?(actual, name)
- @expected_arity.nil?? true : @expected_arity == actual.method(name).arity
+ return true unless @expected_arity
+
+ actual_arity = actual.method(name).arity
+ if actual_arity < 0
+ # ~ inverts the one's complement and gives us the number of required args
+ ~actual_arity <= @expected_arity
+ else
+ actual_arity == @expected_arity
+ end
end
def with_arity
@@ -20,6 +20,18 @@
def obj.foo(arg); end
obj.should respond_to(:foo).with(1).argument
end
+
+ it "passes if target responds to any number of arguments" do
+ obj = Object.new
+ def obj.foo(*args); end
+ obj.should respond_to(:foo).with(1).argument
+ end
+
+ it "passes if target responds to one or more arguments" do
+ obj = Object.new
+ def obj.foo(a, *args); end
+ obj.should respond_to(:foo).with(1).argument
+ end
it "fails if target does not respond to :sym" do
obj = Object.new
@@ -43,6 +55,14 @@ def obj.foo(arg, arg2); end
obj.should respond_to(:foo).with(1).argument
}.should fail_with(/expected #<Object.*> to respond to :foo with 1 argument/)
end
+
+ it "fails if :sym expects 2 or more args" do
+ obj = Object.new
+ def obj.foo(arg, arg2, *args); end
+ lambda {
+ obj.should respond_to(:foo).with(1).argument
+ }.should fail_with(/expected #<Object.*> to respond to :foo with 1 argument/)
+ end
end
describe "should respond_to(message1, message2)" do
@@ -76,6 +96,24 @@ def obj.foo(arg, arg2); end
def obj.foo(a1, a2); end
obj.should respond_to(:foo).with(2).arguments
end
+
+ it "passes if target responds to any number of arguments" do
+ obj = Object.new
+ def obj.foo(*args); end
+ obj.should respond_to(:foo).with(2).arguments
+ end
+
+ it "passes if target responds to one or more arguments" do
+ obj = Object.new
+ def obj.foo(a, *args); end
+ obj.should respond_to(:foo).with(2).arguments
+ end
+
+ it "passes if target responds to two or more arguments" do
+ obj = Object.new
+ def obj.foo(a, b, *args); end
+ obj.should respond_to(:foo).with(2).arguments
+ end
it "fails if target does not respond to :sym" do
obj = Object.new
@@ -92,13 +130,21 @@ def obj.foo; end
}.should fail_with(/expected #<Object.*> to respond to :foo with 2 arguments/)
end
- it "fails if :sym expects 2 args" do
+ it "fails if :sym expects 1 args" do
obj = Object.new
def obj.foo(arg); end
lambda {
obj.should respond_to(:foo).with(2).arguments
}.should fail_with(/expected #<Object.*> to respond to :foo with 2 arguments/)
end
+
+ it "fails if :sym expects 3 or more args" do
+ obj = Object.new
+ def obj.foo(arg, arg2, arg3, *args); end
+ lambda {
+ obj.should respond_to(:foo).with(2).arguments
+ }.should fail_with(/expected #<Object.*> to respond to :foo with 2 arguments/)
+ end
end
describe "should_not respond_to(:sym)" do
@@ -114,3 +160,133 @@ def obj.foo(arg); end
end
end
+
+describe "should_not respond_to(:sym).with(1).argument" do
+ it "fails if target responds to :sym with 1 arg" do
+ obj = Object.new
+ def obj.foo(arg); end
+ lambda {
+ obj.should_not respond_to(:foo).with(1).argument
+ }.should fail_with(/expected #<Object:.*> not to respond to :foo with 1 argument/)
+ end
+
+ it "fails if target responds to :sym with any number of args" do
+ obj = Object.new
+ def obj.foo(*args); end
+ lambda {
+ obj.should_not respond_to(:foo).with(1).argument
+ }.should fail_with(/expected #<Object:.*> not to respond to :foo with 1 argument/)
+ end
+
+ it "fails if target responds to :sym with one or more args" do
+ obj = Object.new
+ def obj.foo(a, *args); end
+ lambda {
+ obj.should_not respond_to(:foo).with(1).argument
+ }.should fail_with(/expected #<Object:.*> not to respond to :foo with 1 argument/)
+ end
+
+ it "passes if target does not respond to :sym" do
+ obj = Object.new
+ obj.should_not respond_to(:some_method).with(1).argument
+ end
+
+ it "passes if :sym expects 0 args" do
+ obj = Object.new
+ def obj.foo; end
+ obj.should_not respond_to(:foo).with(1).argument
+ end
+
+ it "passes if :sym expects 2 args" do
+ obj = Object.new
+ def obj.foo(arg, arg2); end
+ obj.should_not respond_to(:foo).with(1).argument
+ end
+
+ it "passes if :sym expects 2 or more args" do
+ obj = Object.new
+ def obj.foo(arg, arg2, *args); end
+ obj.should_not respond_to(:foo).with(1).argument
+ end
+end
+
+describe "should_not respond_to(message1, message2)" do
+ it "passes if target does not respond to either message1 or message2" do
+ Object.new.should_not respond_to(:some_method, :some_other_method)
+ end
+
+ it "fails if target responds to message1 but not message2" do
+ lambda {
+ Object.new.should_not respond_to(:object_id, :some_method)
+ }.should fail_with(/expected #<Object:.*> not to respond to :object_id/)
+ end
+
+ it "fails if target responds to message2 but not message1" do
+ lambda {
+ Object.new.should_not respond_to(:some_method, :object_id)
+ }.should fail_with(/expected #<Object:.*> not to respond to :object_id/)
+ end
+
+ it "fails if target responds to both message1 and message2" do
+ lambda {
+ Object.new.should_not respond_to(:class, :object_id)
+ }.should fail_with(/expected #<Object:.*> not to respond to :class, :object_id/)
+ end
+end
+
+describe "should_not respond_to(:sym).with(2).arguments" do
+ it "fails if target responds to :sym with 2 args" do
+ obj = Object.new
+ def obj.foo(a1, a2); end
+ lambda {
+ obj.should_not respond_to(:foo).with(2).arguments
+ }.should fail_with(/expected .* not to respond to :foo with 2 arguments/)
+ end
+
+ it "fails if target responds to :sym with any number args" do
+ obj = Object.new
+ def obj.foo(*args); end
+ lambda {
+ obj.should_not respond_to(:foo).with(2).arguments
+ }.should fail_with(/expected .* not to respond to :foo with 2 arguments/)
+ end
+
+ it "fails if target responds to :sym with one or more args" do
+ obj = Object.new
+ def obj.foo(a, *args); end
+ lambda {
+ obj.should_not respond_to(:foo).with(2).arguments
+ }.should fail_with(/expected .* not to respond to :foo with 2 arguments/)
+ end
+
+ it "fails if target responds to :sym with two or more args" do
+ obj = Object.new
+ def obj.foo(a, b, *args); end
+ lambda {
+ obj.should_not respond_to(:foo).with(2).arguments
+ }.should fail_with(/expected .* not to respond to :foo with 2 arguments/)
+ end
+
+ it "passes if target does not respond to :sym" do
+ obj = Object.new
+ obj.should_not respond_to(:some_method).with(2).arguments
+ end
+
+ it "passes if :sym expects 0 args" do
+ obj = Object.new
+ def obj.foo; end
+ obj.should_not respond_to(:foo).with(2).arguments
+ end
+
+ it "passes if :sym expects 2 args" do
+ obj = Object.new
+ def obj.foo(arg); end
+ obj.should_not respond_to(:foo).with(2).arguments
+ end
+
+ it "passes if :sym expects 3 or more args" do
+ obj = Object.new
+ def obj.foo(a, b, c, *arg); end
+ obj.should_not respond_to(:foo).with(2).arguments
+ end
+end

0 comments on commit 64f9e9a

Please sign in to comment.