Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Adds spying methods to the RSpec Mocks DSL

Specifically adds:

* spy - responds and spies all methods
* instance_spy - responds and spies all methods to which a specified
  class responds.
* object_spy - responds and spies all methods to which a specific
  instance responds
* class_spy - responds and spies all class methods to which a specific
  class responds

I added test coverage for the expected behaviours and added some YARD
docs to the new methods.
  • Loading branch information...
commit 0fd22663357dbb3ba55c11913dfbb04fbbcfcec7 1 parent 1c89e97
Sam Phippen samphippen authored
9 Changelog.md
View
@@ -1,3 +1,12 @@
+### 3.1.0 Development
+[Full Changelog](http://github.com/rspec/rspec-mocks/compare/v3.0.0...master)
+
+Enhancements:
+
+* Add spying methods (`spy`, `ìnstance_spy`, `class_spy` and `object_spy`)
+ which create doubles as null objects for use with spying in testing. (Sam
+ Phippen, #671)
+
### 3.0.1 / 2014-06-07
[Full Changelog](http://github.com/rspec/rspec-mocks/compare/v3.0.0...v3.0.1)
22 README.md
View
@@ -101,15 +101,24 @@ zipcode.valid?
## Test Spies
-Verifies the given object received the expected message during the course of the
-test. The method must have previously been stubbed in order for messages to be
-verified.
+Verifies the given object received the expected message during the course of
+the test. For a message to be verified, the given object must be setup to spy
+on it, either by having it explicitly stubbed or by being a null object double
+(e.g. `double(...).as_null_object`). Convenience methods are provided to easily
+create null object doubles for this purpose:
+
+```ruby
+spy("invitation") # => same as `double("invitiation").as_null_object`
+instance_spy("Invitation") # => same as `instance_double("Invitiation").as_null_object`
+class_spy("Invitation") # => same as `class_double("Invitiation").as_null_object`
+object_spy("Invitation") # => same as `object_double("Invitiation").as_null_object`
+```
Stubbing and verifying messages received in this way implements the Test Spy
pattern.
```ruby
- invitation = double('invitation', :accept => true)
+ invitation = spy('invitation')
user.accept_invitation(invitation)
@@ -119,6 +128,11 @@ pattern.
expect(invitation).to have_received(:accept).with(mailer)
expect(invitation).to have_received(:accept).twice
expect(invitation).to_not have_received(:accept).with(mailer)
+
+ # One can specify a return value on the spy the same way one would a double.
+ invitation = spy('invitation', :accept => true)
+ expect(invitation).to have_received(:accept).with(mailer)
+ expect(invitation.accept).to eq(true)
```
## Nomenclature
20 features/basics/spies.feature
View
@@ -7,25 +7,25 @@ Feature: Spies
`have_received`.
You can use any test double (or partial double) as a spy, but the double must be setup to
- spy on the messages you care about. [Null object doubles](./null-object-doubles) automatically spy on all messages,
+ spy on the messages you care about. Spies automatically spy on all messages,
or you can [allow a message](./allowing-messages) to spy on it.
`have_received` supports the same fluent interface for [setting constraints](../setting-constraints) that normal message expectations do.
Note: The `have_received` API shown here will only work if you are using rspec-expectations.
- Scenario: Use a null object double as a spy
- Given a file named "null_object_spy_spec.rb" with:
+ Scenario: Using a spy
+ Given a file named "spy_spec.rb" with:
"""ruby
RSpec.describe "have_received" do
it "passes when the message has been received" do
- invitation = double('invitation').as_null_object
+ invitation = spy('invitation')
invitation.deliver
expect(invitation).to have_received(:deliver)
end
end
"""
- When I run `rspec null_object_spy_spec.rb`
+ When I run `rspec spy_spec.rb`
Then the examples should all pass
Scenario: Spy on a method on a partial double
@@ -54,8 +54,8 @@ Feature: Spies
end
RSpec.describe "failure when the message has not been received" do
- example "for a null object double" do
- invitation = double('invitation').as_null_object
+ example "for a spy" do
+ invitation = spy('invitation')
expect(invitation).to have_received(:deliver)
end
@@ -68,7 +68,7 @@ Feature: Spies
When I run `rspec failure_spec.rb --order defined`
Then it should fail with:
"""
- 1) failure when the message has not been received for a null object double
+ 1) failure when the message has not been received for a spy
Failure/Error: expect(invitation).to have_received(:deliver)
(Double "invitation").deliver(any args)
expected: 1 time with any arguments
@@ -87,7 +87,7 @@ Feature: Spies
Given a file named "setting_constraints_spec.rb" with:
"""ruby
RSpec.describe "An invitiation" do
- let(:invitation) { double("invitation").as_null_object }
+ let(:invitation) { spy("invitation") }
before do
invitation.deliver("foo@example.com")
@@ -131,7 +131,7 @@ Feature: Spies
Given a file named "generates_description_spec.rb" with:
"""ruby
RSpec.describe "An invitation" do
- subject(:invitation) { double('invitation').as_null_object }
+ subject(:invitation) { spy('invitation') }
before { invitation.deliver }
it { is_expected.to have_received(:deliver) }
end
4 lib/rspec/mocks/error_generator.rb
View
@@ -169,13 +169,13 @@ def raise_only_valid_on_a_partial_double(method)
# @private
def raise_expectation_on_unstubbed_method(method)
__raise "#{intro} expected to have received #{method}, but that " +
- "method has not been stubbed."
+ "object is not a spy or method has not been stubbed."
end
# @private
def raise_expectation_on_mocked_method(method)
__raise "#{intro} expected to have received #{method}, but that " +
- "method has been mocked instead of stubbed."
+ "method has been mocked instead of stubbed or spied."
end
def self.raise_double_negation_error(wrapped_expression)
72 lib/rspec/mocks/example_methods.rb
View
@@ -84,6 +84,73 @@ def object_double(object_or_name, *args)
ExampleMethods.declare_verifying_double(ObjectVerifyingDouble, ref, *args)
end
+ # @overload spy()
+ # @overload spy(name)
+ # @param name [String/Symbol] used to clarify intent
+ # @overload spy(stubs)
+ # @param stubs (Hash) hash of message/return-value pairs
+ # @overload spy(name, stubs)
+ # @param name [String/Symbol] used to clarify intent
+ # @param stubs (Hash) hash of message/return-value pairs
+ # @return (Double)
+ #
+ # Constructs a test double that is optimized for use with
+ # `have_received`. With a normal double one has to stub methods in order
+ # to be able to spy them. A spy automatically spies on all methods.
+ def spy(*args)
+ double(*args).as_null_object
+ end
+
+ # @overload instance_spy(doubled_class)
+ # @param doubled_class [String, Class]
+ # @overload instance_spy(doubled_class, stubs)
+ # @param doubled_class [String, Class]
+ # @param stubs [Hash] hash of message/return-value pairs
+ # @return InstanceVerifyingDouble
+ #
+ # Constructs a test double that is optimized for use with `have_received`
+ # against a specific class. If the given class name has been loaded, only
+ # instance methods defined on the class are allowed to be stubbed. With
+ # a normal double one has to stub methods in order to be able to spy
+ # them. An instance_spy automatically spies on all instance methods to
+ # which the class responds.
+ def instance_spy(*args)
+ instance_double(*args).as_null_object
+ end
+
+ # @overload object_spy(object_or_name)
+ # @param object_or_name [String, Object]
+ # @overload object_spy(object_or_name, stubs)
+ # @param object_or_name [String, Object]
+ # @param stubs [Hash] hash of message/return-value pairs
+ # @return ObjectVerifyingDouble
+ #
+ # Constructs a test double that is optimized for use with `have_received`
+ # against a specific object. Only instance methods defined on the object
+ # are allowed to be stubbed. With a normal double one has to stub
+ # methods in order to be able to spy them. An object_spy automatically
+ # spies on all methods to which the object responds.
+ def object_spy(*args)
+ object_double(*args).as_null_object
+ end
+
+ # @overload class_spy(doubled_class)
+ # @param doubled_class [String, Module]
+ # @overload class_spy(doubled_class, stubs)
+ # @param doubled_class [String, Module]
+ # @param stubs [Hash] hash of message/return-value pairs
+ # @return ClassVerifyingDouble
+ #
+ # Constructs a test double that is optimized for use with `have_received`
+ # against a specific class. If the given class name has been loaded,
+ # only class methods defined on the class are allowed to be stubbed.
+ # With a normal double one has to stub methods in order to be able to spy
+ # them. An class_spy automatically spies on all class methods to which the
+ # class responds.
+ def class_spy(*args)
+ class_double(*args).as_null_object
+ end
+
# Disables warning messages about expectations being set on nil.
#
# By default warning messages are issued when expectations are set on
@@ -151,8 +218,9 @@ def hide_const(constant_name)
end
# Verifies that the given object received the expected message during the
- # course of the test. The method must have previously been stubbed in
- # order for messages to be verified.
+ # course of the test. On a spy objects or as null object doubles this
+ # works for any method, on other objects the method must have
+ # been stubbed beforehand in order for messages to be verified.
#
# Stubbing and verifying messages received in this way implements the
# Test Spy pattern.
115 spec/rspec/mocks/spy_spec.rb
View
@@ -0,0 +1,115 @@
+require "spec_helper"
+
+describe "the spy family of methods" do
+ describe "spy" do
+ it "responds to arbitrary methods" do
+ expect(spy.respond_to?(:foo)).to be true
+ end
+
+ it "takes a name" do
+ expect(spy(:bacon_bits).inspect).to include("bacon_bits")
+ end
+
+ it "records called methods" do
+ expect(spy.tap { |s| s.foo }).to have_received(:foo)
+ end
+
+ it "takes a hash of method names and return values" do
+ expect(spy(:foo => :bar).foo).to eq(:bar)
+ end
+
+ it "takes a name and a hash of method names and return values" do
+ expect(spy(:bacon_bits, :foo => :bar).foo).to eq(:bar)
+ end
+ end
+
+ shared_examples_for "a verifying spy with a foo method" do
+ it "responds to methods on the verified object" do
+ expect(subject.respond_to?(:foo)).to be true
+ end
+
+ it "does not respond to methods that are not on the verified object" do
+ expect(subject.respond_to?(:other_method)).to be false
+ end
+
+ it "records called methods" do
+ expect(subject.tap { |s| s.foo}).to have_received(:foo)
+ end
+ end
+
+ describe "instance_spy" do
+ context "when passing a class object" do
+ let(:the_class) do
+ Class.new do
+ def foo
+ 3
+ end
+ end
+ end
+
+ subject { instance_spy(the_class) }
+
+ it_behaves_like "a verifying spy with a foo method"
+
+ it "takes a class and a hash of method names and return values" do
+ expect(instance_spy(the_class, :foo => :bar).foo).to eq(:bar)
+ end
+ end
+
+ context "passing a class by string reference" do
+ DummyClass = Class.new do
+ def foo
+ 3
+ end
+ end
+
+ let(:the_class) { "DummyClass" }
+
+ subject { instance_spy(the_class) }
+
+ it_behaves_like "a verifying spy with a foo method"
+
+ it "takes a class name string and a hash of method names and return values" do
+ expect(instance_spy(the_class, :foo => :bar).foo).to eq(:bar)
+ end
+ end
+ end
+
+ describe "object_spy" do
+ let(:the_class) do
+ Class.new do
+ def foo
+ 3
+ end
+ end
+ end
+
+ let(:the_instance) { the_class.new }
+
+ subject { object_spy(the_instance) }
+
+ it_behaves_like "a verifying spy with a foo method"
+
+ it "takes an instance and a hash of method names and return values" do
+ expect(object_spy(the_instance, :foo => :bar).foo).to eq(:bar)
+ end
+ end
+
+ describe "class_spy" do
+ let(:the_class) do
+ Class.new do
+ def self.foo
+ 3
+ end
+ end
+ end
+
+ subject { class_spy(the_class) }
+
+ it_behaves_like "a verifying spy with a foo method"
+
+ it "takes a class and a hash of method names and return values" do
+ expect(class_spy(the_class, :foo => :bar).foo).to eq(:bar)
+ end
+ end
+end
Please sign in to comment.
Something went wrong with that request. Please try again.