Browse files

Add support for any_instance:

- MyClass.any_instance.stub(:m)
- MyClass.any_instance.should_receive(:m)
- includes stub and mock APIs including
  - and_return, and_raise, and_yield
  - once, twice, exactly, any_number_of_times, never, at_least, at_most

- Closes #46.
- Closes #10.
  • Loading branch information...
1 parent 52356e5 commit 699a5c20e462ca04dde91fb80eedeb36f7244292 @kaiwren kaiwren committed Mar 24, 2011
Showing with 573 additions and 0 deletions.
  1. +1 −0 lib/rspec/mocks.rb
  2. +211 −0 lib/rspec/mocks/any_instance.rb
  3. +1 −0 lib/rspec/mocks/framework.rb
  4. +360 −0 spec/rspec/mocks/any_instance_spec.rb
View
1 lib/rspec/mocks.rb
@@ -178,6 +178,7 @@ class << self
def setup(includer)
Object.class_eval { include RSpec::Mocks::Methods } unless Object < RSpec::Mocks::Methods
+ Class.class_eval { include RSpec::Mocks::AnyInstance }
(class << includer; self; end).class_eval do
include RSpec::Mocks::ExampleMethods
end
View
211 lib/rspec/mocks/any_instance.rb
@@ -0,0 +1,211 @@
+module RSpec
+ module Mocks
+ module AnyInstance
+ class Chain
+ def initialize(rspec_method_name, method_name, *args, &block)
+ @messages = []
+ record(rspec_method_name, [method_name] + args, block)
+ end
+
+ [
+ :with, :and_return, :and_raise, :and_yield,
+ :once, :twice, :any_number_of_times,
+ :exactly, :times, :never,
+ :at_least, :at_most
+ ].each do |method_name|
+ dispatch_method_definition = <<-EOM
+ def #{method_name}(*args, &block)
+ record(:#{method_name}, args, block)
+ end
+ EOM
+ class_eval(dispatch_method_definition, __FILE__, __LINE__)
+ end
+
+ def playback!(instance)
+ @messages.inject(instance) do |instance, message|
+ instance.__send__(*message.first, &message.last)
+ end
+ end
+
+ private
+ def verify_invocation_order(rspec_method_name, args, block)
+ # implement in subclasses
+ raise NotImplementedError
+ end
+
+ def last_message
+ @messages.last.first.first unless @messages.empty?
+ end
+
+ def record(rspec_method_name, args, block)
+ verify_invocation_order(rspec_method_name, args, block)
+ @messages << [args.unshift(rspec_method_name), block]
+ self
+ end
+ end
+
+ class StubChain < Chain
+ def invocation_order
+ @invocation_order ||= {
+ :stub => [nil],
+ :with => [:stub],
+ :and_return => [:with, :stub],
+ :and_raise => [:with, :stub],
+ :and_yield => [:with, :stub]
+ }
+ end
+
+ def initialize(*args, &block)
+ super(:stub, *args, &block)
+ end
+
+ private
+ def verify_invocation_order(rspec_method_name, args, block)
+ if !invocation_order[rspec_method_name].include?(last_message)
+ raise(NoMethodError, "Undefined method #{rspec_method_name}")
+ end
+ end
+ end
+
+ class ExpectationChain < Chain
+ def invocation_order
+ @invocation_order ||= {
+ :should_receive => [nil],
+ :with => [:should_receive],
+ :and_return => [:with, :should_receive],
+ :and_raise => [:with, :should_receive]
+ }
+ end
+
+ def initialize(*args, &block)
+ super(:should_receive, *args, &block)
+ end
+
+ private
+ def verify_invocation_order(rspec_method_name, args, block)
+ end
+ end
+
+ class Recorder
+ def initialize(klass)
+ @observed_methods = {}
+ @played_methods = []
+ @klass = klass
+ end
+
+ def stub(method_name, *args, &block)
+ observe!(method_name)
+ @observed_methods[method_name.to_sym] = StubChain.new(method_name, *args, &block)
+ end
+
+ def should_receive(method_name, *args, &block)
+ observe!(method_name)
+ @observed_methods[method_name.to_sym] = ExpectationChain.new(method_name, *args, &block)
+ end
+
+ def stop_observing_currently_observed_methods!
+ observed_method_names.each do |method_name|
+ stop_observing!(method_name)
+ end
+ end
+
+ def playback_to_uninvoked_observed_methods_with_expectations!(instance)
+ @observed_methods.each do |method_name, chain|
+ case chain
+ when ExpectationChain
+ chain.playback!(instance) unless @played_methods.include?(method_name)
+ end
+ end
+ end
+
+ def playback!(instance, method_name)
+ RSpec::Mocks::space.add(instance) if RSpec::Mocks::space
+ @observed_methods[method_name].playback!(instance)
+ @played_methods << method_name
+ end
+
+ private
+ def observed_method_names
+ @observed_methods.keys
+ end
+
+ def build_alias_method_name(method_name)
+ "__#{method_name}_without_any_instance__".to_sym
+ end
+
+ def stop_observing!(method_name)
+ if @klass.instance_methods.include?(build_alias_method_name(method_name))
+ restore_original_method!(method_name)
+ else
+ remove_dummy_method!(method_name)
+ end
+ @observed_methods.delete(method_name)
+ end
+
+ def restore_original_method!(method_name)
+ alias_method_name = build_alias_method_name(method_name)
+ @klass.class_eval do
+ alias_method method_name, alias_method_name
+ remove_method alias_method_name
+ end
+ end
+
+ def remove_dummy_method!(method_name)
+ @klass.class_eval do
+ remove_method method_name
+ end
+ end
+
+ def observe!(method_name)
+ alias_method_name = build_alias_method_name(method_name)
+ @klass.class_eval do
+ if instance_methods.include?(method_name)
+ alias_method alias_method_name, method_name
+ end
+ end
+ method = <<-EOM
+ def #{method_name}(*args, &blk)
+ self.class.__recorder.playback!(self, :#{method_name})
+ self.send(:#{method_name}, *args, &blk)
+ end
+ EOM
+ @klass.class_eval(method, __FILE__, __LINE__)
+ end
+ end
+
+ module ExpectationEnsurer
+ def rspec_verify
+ self.class.__recorder.playback_to_uninvoked_observed_methods_with_expectations!(self)
+ super
+ end
+ end
+
+ def any_instance
+ RSpec::Mocks::space.add(self) if RSpec::Mocks::space
+ self.class_eval{ include ExpectationEnsurer }
+ __recorder
+ end
+
+ def rspec_verify
+ super
+ ensure
+ rspec_reset
+ end
+
+ def rspec_reset
+ __recorder.stop_observing_currently_observed_methods!
+ @__recorder = nil
+ response = super
+ response
+ end
+
+ def reset?
+ !@__recorder && super
+ end
+
+ def __recorder
+ @__recorder ||= AnyInstance::Recorder.new(self)
+ end
+ end
+ end
+end
View
1 lib/rspec/mocks/framework.rb
@@ -15,3 +15,4 @@
require 'rspec/mocks/error_generator'
require 'rspec/mocks/space'
require 'rspec/mocks/serialization'
+require 'rspec/mocks/any_instance'
View
360 spec/rspec/mocks/any_instance_spec.rb
@@ -0,0 +1,360 @@
+require 'spec_helper'
+
+module RSpec
+ module Mocks
+ describe "#any_instance" do
+ class CustomErrorForTesting < StandardError;end
+ let(:klass) do
+ klass = Class.new
+ klass.class_eval{ def ooga;2;end }
+ klass
+ end
+
+ context "invocation order" do
+ context "#stub" do
+ it "raises an error if 'stub' follows 'with'" do
+ lambda{ klass.any_instance.with("1").stub(:foo) }.should raise_error(NoMethodError)
+ end
+
+ it "raises an error if 'with' follows 'and_return'" do
+ lambda{ klass.any_instance.stub(:foo).and_return(1).with("1") }.should raise_error(NoMethodError)
+ end
+
+ it "raises an error if 'with' follows 'and_raise'" do
+ lambda{ klass.any_instance.stub(:foo).and_raise(1).with("1") }.should raise_error(NoMethodError)
+ end
+
+ it "raises an error if 'with' follows 'and_yield'" do
+ lambda{ klass.any_instance.stub(:foo).and_yield(1).with("1") }.should raise_error(NoMethodError)
+ end
+ end
+
+ context "#should_receive" do
+ it "raises an error if 'should_receive' follows 'with'" do
+ lambda{ klass.any_instance.with("1").should_receive(:foo) }.should raise_error(NoMethodError)
+ end
+
+ it "raises an error if 'with' follows 'and_return'" do
+ pending "see Github issue #42"
+ lambda{ klass.any_instance.should_receive(:foo).and_return(1).with("1") }.should raise_error(NoMethodError)
+ end
+
+ it "raises an error if 'with' follows 'and_raise'" do
+ pending "see Github issue #42"
+ lambda{ klass.any_instance.should_receive(:foo).and_raise(1).with("1") }.should raise_error(NoMethodError)
+ end
+ end
+ end
+
+ context "with #stub" do
+ it "should not suppress an exception when a method that doesn't exist is invoked" do
+ klass.any_instance.stub(:foo)
+ lambda{ klass.new.bar }.should raise_error(NoMethodError)
+ end
+
+ context "with #and_return" do
+ it "stubs a method that doesn't exist on any instance of a particular class" do
+ klass.any_instance.stub(:foo).and_return(1)
+ klass.new.foo.should == 1
+ end
+
+ it "stubs a method that exists on any instance of a particular class" do
+ klass.any_instance.stub(:ooga).and_return(1)
+ klass.new.ooga.should == 1
+ end
+
+ it "returns the same object for calls on different instances" do
+ return_value = Object.new
+ klass.any_instance.stub(:foo).and_return(return_value)
+ klass.new.foo.should be(return_value)
+ klass.new.foo.should be(return_value)
+ end
+ end
+
+ context "with #and_yield" do
+ it "yields the value specified" do
+ yielded_value = Object.new
+ klass.any_instance.stub(:foo).and_yield(yielded_value)
+
+ klass.new.foo{|value| value.should be(yielded_value)}
+ end
+ end
+
+ context "with #and_raise" do
+ it "stubs a method that doesn't exist on any instance of a particular class" do
+ klass.any_instance.stub(:foo).and_raise(CustomErrorForTesting)
+ lambda{ klass.new.foo}.should raise_error(CustomErrorForTesting)
+ end
+
+ it "stubs a method that exists on any instance of a particular class" do
+ klass.any_instance.stub(:ooga).and_raise(CustomErrorForTesting)
+ lambda{ klass.new.ooga}.should raise_error(CustomErrorForTesting)
+ end
+ end
+
+ context "with a block" do
+ it "stubs a method on any instance of a particular class" do
+ klass.any_instance.stub(:foo) { 1 }
+ klass.new.foo.should == 1
+ end
+
+ it "returns the same computed value for calls on different instances" do
+ klass.any_instance.stub(:foo) { 1 + 2 }
+ klass.new.foo.should == klass.new.foo
+ end
+ end
+
+ context "core ruby objects" do
+ it "should work uniformly across *everything*" do
+ Object.any_instance.stub(:foo).and_return(1)
+ Object.new.foo.should == 1
+ end
+
+ it "should work with the non-standard constructor []" do
+ Array.any_instance.stub(:foo).and_return(1)
+ [].foo.should == 1
+ end
+
+ it "should work with the non-standard constructor {}" do
+ Hash.any_instance.stub(:foo).and_return(1)
+ {}.foo.should == 1
+ end
+
+ it "should work with the non-standard constructor \"\"" do
+ String.any_instance.stub(:foo).and_return(1)
+ "".foo.should == 1
+ end
+
+ it "should work with the non-standard constructor \'\'" do
+ String.any_instance.stub(:foo).and_return(1)
+ ''.foo.should == 1
+ end
+
+ it "should work with the non-standard constructor module" do
+ Module.any_instance.stub(:foo).and_return(1)
+ module RSpec::SampleRspecTestModule;end
+ RSpec::SampleRspecTestModule.foo.should == 1
+ end
+
+ it "should work with the non-standard constructor class" do
+ Class.any_instance.stub(:foo).and_return(1)
+ class RSpec::SampleRspecTestClass;end
+ RSpec::SampleRspecTestClass.foo.should == 1
+ end
+ end
+ end
+
+ context "with #should_receive" do
+ context "when the method on which the expectation is set doesn't exist" do
+ it "returns the expected value" do
+ klass.any_instance.should_receive(:foo).and_return(1)
+ klass.new.foo(1).should == 1
+ end
+
+ it "fails the verification if an instance is created but no invocation occurs" do
+ expect do
+ klass.any_instance.should_receive(:foo)
+ klass.new.rspec_verify
+ end.to raise_error(RSpec::Mocks::MockExpectationError)
+ end
+
+ it "does nothing if no instance is created" do
+ klass.any_instance.should_receive(:foo).and_return(1)
+ end
+ end
+
+ context "when an expectation is set on a method that exists" do
+ it "returns the expected value" do
+ klass.any_instance.should_receive(:ooga).and_return(1)
+ klass.new.ooga(1).should == 1
+ end
+
+ it "fails the verification if an instance is created but no invocation occurs" do
+ expect do
+ klass.any_instance.should_receive(:ooga)
+ instance = klass.new
+ instance.rspec_verify
+ end.to raise_error(RSpec::Mocks::MockExpectationError)
+ end
+
+ it "does nothing if no instance is created" do
+ klass.any_instance.should_receive(:ooga).and_return(1)
+ end
+ end
+
+ context "resetting" do
+ it "does not interfere with expectations set on the class" do
+ expect do
+ klass.should_receive(:woot).and_return(3)
+ klass.rspec_verify
+ end.to raise_error(RSpec::Mocks::MockExpectationError)
+ end
+ end
+
+ context "message count" do
+ context "the 'once' constraint" do
+ it "passes for one invocation" do
+ klass.any_instance.should_receive(:foo).once
+ instance = klass.new
+ instance.foo
+ end
+
+ it "fails for more than one invocation" do
+ expect do
+ klass.any_instance.should_receive(:foo).once
+ instance = klass.new
+ 2.times{ instance.foo }
+ instance.rspec_verify
+ end.to raise_error(RSpec::Mocks::MockExpectationError)
+ end
+ end
+
+ context "the 'twice' constraint" do
+ it "passes for two invocations" do
+ klass.any_instance.should_receive(:foo).twice
+ instance = klass.new
+ 2.times{ instance.foo }
+ end
+
+ it "fails for more than two invocations" do
+ expect do
+ klass.any_instance.should_receive(:foo).twice
+ instance = klass.new
+ 3.times{ instance.foo }
+ instance.rspec_verify
+ end.to raise_error(RSpec::Mocks::MockExpectationError)
+ end
+ end
+
+ context "the 'exactly(n)' constraint" do
+ it "passes for n invocations where n = 3" do
+ klass.any_instance.should_receive(:foo).exactly(3).times
+ instance = klass.new
+ 3.times{ instance.foo }
+ end
+
+ it "fails for n invocations where n < 3" do
+ expect do
+ klass.any_instance.should_receive(:foo).exactly(3).times
+ instance = klass.new
+ 2.times{ instance.foo }
+ instance.rspec_verify
+ end.to raise_error(RSpec::Mocks::MockExpectationError)
+ end
+
+ it "fails for n invocations where n > 3" do
+ expect do
+ klass.any_instance.should_receive(:foo).exactly(3).times
+ instance = klass.new
+ 4.times{ instance.foo }
+ instance.rspec_verify
+ end.to raise_error(RSpec::Mocks::MockExpectationError)
+ end
+ end
+
+ context "the 'at_least(n)' constraint" do
+ it "passes for n invocations where n = 3" do
+ klass.any_instance.should_receive(:foo).at_least(3).times
+ instance = klass.new
+ 3.times{ instance.foo }
+ end
+
+ it "fails for n invocations where n < 3" do
+ expect do
+ klass.any_instance.should_receive(:foo).at_least(3).times
+ instance = klass.new
+ 2.times{ instance.foo }
+ instance.rspec_verify
+ end.to raise_error(RSpec::Mocks::MockExpectationError)
+ end
+
+ it "passes for n invocations where n > 3" do
+ klass.any_instance.should_receive(:foo).at_least(3).times
+ instance = klass.new
+ 4.times{ instance.foo }
+ end
+ end
+
+ context "the 'at_most(n)' constraint" do
+ it "passes for n invocations where n = 3" do
+ klass.any_instance.should_receive(:foo).at_most(3).times
+ instance = klass.new
+ 3.times{ instance.foo }
+ end
+
+ it "passes for n invocations where n < 3" do
+ klass.any_instance.should_receive(:foo).at_most(3).times
+ instance = klass.new
+ 2.times{ instance.foo }
+ end
+
+ it "fails for n invocations where n > 3" do
+ expect do
+ klass.any_instance.should_receive(:foo).at_most(3).times
+ instance = klass.new
+ 4.times{ instance.foo }
+ instance.rspec_verify
+ end.to raise_error(RSpec::Mocks::MockExpectationError)
+ end
+ end
+
+ context "the 'never' constraint" do
+ it "passes for 0 invocations" do
+ klass.any_instance.should_receive(:foo).never
+ klass.new
+ end
+
+ it "fails on the first invocation" do
+ expect do
+ klass.any_instance.should_receive(:foo).never
+ instance = klass.new
+ instance.foo
+ instance.rspec_verify
+ end.to raise_error(RSpec::Mocks::MockExpectationError)
+ end
+ end
+
+ context "the 'any_number_of_times' constraint" do
+ it "passes for 0 invocations" do
+ klass.any_instance.should_receive(:foo).any_number_of_times
+ klass.new.rspec_verify
+ end
+
+ it "passes for a non-zero number of invocations" do
+ klass.any_instance.should_receive(:foo).any_number_of_times
+ instance = klass.new
+ instance.foo
+ end
+ end
+ end
+ end
+
+ context "when resetting after an example" do
+ it "restores the class to its original state after each example" do
+ space = RSpec::Mocks::Space.new
+ space.add(klass)
+
+ klass.any_instance.stub(:ooga).and_return(1)
+ klass.instance_methods.should include(:__ooga_without_any_instance__)
+
+ space.reset_all
+
+ klass.instance_methods.grep(/ooga/).should_not include(:__ooga_without_any_instance__)
+ klass.new.ooga.should == 2
+ end
+
+ it "adds an class to the current space when #any_instance is invoked" do
+ klass.any_instance
+ RSpec::Mocks::space.send(:mocks).should include(klass)
+ end
+
+ it "adds an instance to the current space" do
+ klass.any_instance.stub(:foo)
+ instance = klass.new
+ instance.foo
+ RSpec::Mocks::space.send(:mocks).should include(instance)
+ end
+ end
+ end
+ end
+end

0 comments on commit 699a5c2

Please sign in to comment.