Skip to content

Commit 66c78a9

Browse files
committed
add documentation and and_return raises error if block is sent with wrong arity
1 parent 5723617 commit 66c78a9

File tree

7 files changed

+139
-77
lines changed

7 files changed

+139
-77
lines changed

.yardopts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
--markup=markdown

Gemfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,5 @@ source 'https://rubygems.org'
44
gemspec
55
gem 'pry'
66
gem 'pry-nav'
7+
gem 'yard'
8+
gem 'redcarpet'

README.md

Lines changed: 73 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# Spy
22

3-
Spy is a lightweight stubbing framework that won't let your code mock your intelligence.
3+
Spy is a lightweight stubbing framework with support for method spies, constant stubs, and object doubles.
44

5-
Spy was designed for 1.9.3+ so there is no legacy tech debt.
5+
Spy was designed for 1.9.3+.
66

77
Spy features that were completed were tested against the rspec-mocks tests so it covers all cases that rspec-mocks does.
88

@@ -33,10 +33,7 @@ Fail faster, code faster.
3333
* missing these features
3434
* Mocking null objects
3535
* argument matchers for Spy::Method#has\_been\_called\_with
36-
* watch all calls to an object to check order in which they are called?
37-
* is this useful?
38-
* fail lazily on method call not on hook to allow for dynamic method creation?
39-
* do more than 0.5% of develoeprs use this?
36+
* watch all calls to an object to check order in which they are called
4037

4138
## Installation
4239

@@ -54,64 +51,93 @@ Or install it yourself as:
5451

5552
## Usage
5653

54+
### Method Stubs
55+
56+
A method stub overrides a pre-existing method and records all calls to specified method. You can set the spy to return either the original method or your own custom implementation.
57+
58+
Spy support 2 different ways of spying an existing method on an object.
5759
```ruby
58-
class Person
59-
def first_name
60-
"John"
61-
end
60+
Spy.on(book, title: "East of Eden")
61+
Spy.on(book, :title).and_return("East of Eden")
62+
Spy.on(book, :title).and_return { "East of Eden" }
63+
```
6264

63-
def last_name
64-
"Smith"
65-
end
65+
Spy will raise an error if you try to stub on a method that doesn't exist.
66+
You can force the creation of a sstub on method that didn't exist but it really isn't suggested.
6667

67-
def full_name
68-
"#{first_name} #{last_name}"
69-
end
68+
```ruby
69+
Spy.new(book, :flamethrower).hook(force:true).and_return("burnninante")
70+
```
7071

71-
def say(words)
72-
puts words
73-
end
72+
### Test Doubles
73+
74+
A test double is an object that stands in for a real object.
75+
76+
```ruby
77+
Spy.double("book")
78+
```
79+
80+
Spy will let you stub on any method even if it doesn't exist if the object is a double.
81+
82+
Spy comes with a shortcut to define an object with methods.
83+
84+
```ruby
85+
Spy.double("book", title: "Grapes of Wrath", author: "John Steinbeck")
86+
```
87+
88+
### Arbitrary Handling
89+
90+
If you need to have a custom method based in the method inputs just send a block to #and\_return
91+
92+
```ruby
93+
Spy.on(book, :read_page).and_return do |page, &block|
94+
block.call
95+
"awesome " * page
7496
end
7597
```
7698

77-
### Standalone
99+
An error will raise if the arity of the block is larger than the arity of the original method. However this can be overidden with the force argument.
78100

79101
```ruby
80-
person = Person.new
102+
Spy.on(book, :read_page).and_return(force: true) do |a, b, c, d|
103+
end
104+
```
81105

82-
first_name_spy = Spy.on(person, :first_name)
83-
person.first_name #=> nil
84-
first_name_spy.has_been_called? #=> true
106+
### Method Spies
85107

86-
Spy.get(person, :first_name) #=> first_name_spy
108+
When you stub a method it returns a spy. A spy records what calls have been made to a given method.
87109

88-
Spy.off(person, :first_name)
89-
person.first_name #=> "John"
110+
```ruby
111+
validator = Spy.double("validator")
112+
validate_spy = Spy.on(validator, :validate)
113+
validate_spy.has_been_called? #=> false
114+
validator.validate("01234") #=> nil
115+
validate_spy.has_been_called? #=> true
116+
validate_spy.has_been_called_with?("01234) #=> true
117+
```
90118
91-
first_name_spy.hook #=> first_name_spy
92-
first_name_spy.and_return("Bob")
93-
person.first_name #=> "Bob"
119+
### Calling through
120+
If you just want to make sure if a method is called and not override the output you can just use the and\_call\_through method
94121
95-
Spy.teardown
96-
person.first_name #=> "John"
122+
```ruby
123+
Spy.on(book, :read_page).and_call_through
124+
```
97125
98-
say_spy = Spy.on(person, :say)
99-
person.say("hello") {
100-
"everything accepts a block in ruby"
101-
}
102-
say_spy.say("world")
126+
### Call Logs
103127
104-
say_spy.has_been_called? #=> true
105-
say_spy.has_been_called_with?("hello") #=> true
106-
say_spy.calls.count #=> 1
107-
say_spy.calls.first.args #=> ["hello"]
108-
say_spy.calls.last.args #=> ["world"]
128+
When a spy is called on it records a call log. A call log contains the object it was called on, the arguments and block that were sent to method and what it returned.
109129
110-
call_log = say_spy.calls.first
111-
call_log.object #=> #<Person:0x00000000b2b858>
112-
call_log.args #=> ["hello"]
113-
call_log.block #=> #<Proc:0x00000000b1a9e0>
114-
call_log.block.call #=> "everything accepts a block in ruby"
130+
```ruby
131+
read_page_spy = Spy.on(book, read_page: "hello world")
132+
book.read_page(5) { "this is a block" }
133+
book.read_page(3)
134+
book.read_page(7)
135+
read_page_spy.calls.size #=> 3
136+
first_call = read_page_spy.calls.first
137+
first_call.object #=> book
138+
first_call.args #=> [5]
139+
first_call.block #=> Proc.new { "this is a block" }
140+
first_call.result #=> "hello world"
115141
```
116142
117143
### MiniTest

lib/spy.rb

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
require "spy/agency"
33
require "spy/constant"
44
require "spy/double"
5-
require "spy/dsl"
65
require "spy/nest"
76
require "spy/subroutine"
87
require "spy/version"
@@ -11,10 +10,9 @@ module Spy
1110
SECRET_SPY_KEY = Object.new
1211
class << self
1312
# create a spy on given object
14-
# @params base_object
15-
# @params *method_names [Symbol] will spy on these methods
16-
# @params *method_names [Hash] will spy on these methods and also set default return values
17-
# @return [Spy, Array<Spy>]
13+
# @param base_object
14+
# @param method_names *[Hash,Symbol] will spy on these methods and also set default return values
15+
# @return [Subroutine, Array<Subroutine>]
1816
def on(base_object, *method_names)
1917
spies = method_names.map do |method_name|
2018
create_and_hook_spy(base_object, method_name)
@@ -24,9 +22,9 @@ def on(base_object, *method_names)
2422
end
2523

2624
# removes the spy from the from the given object
27-
# @params base_object
28-
# @params *method_names
29-
# @return [Spy, Array<Spy>]
25+
# @param base_object
26+
# @param method_names *[Symbol]
27+
# @return [Subroutine, Array<Subroutine>]
3028
def off(base_object, *method_names)
3129
removed_spies = method_names.map do |method_name|
3230
spy = Subroutine.get(base_object, method_name)
@@ -40,6 +38,10 @@ def off(base_object, *method_names)
4038
removed_spies.size > 1 ? removed_spies : removed_spies.first
4139
end
4240

41+
# create a stub for constants on given module
42+
# @param base_module [Module]
43+
# @param constant_names *[Symbol, Hash]
44+
# @return [Constant, Array<Constant>]
4345
def on_const(base_module, *constant_names)
4446
if base_module.is_a? Symbol
4547
constant_names.unshift(base_module)
@@ -61,6 +63,10 @@ def on_const(base_module, *constant_names)
6163
spies.size > 1 ? spies : spies.first
6264
end
6365

66+
# removes stubs from given module
67+
# @param base_module [Module]
68+
# @param constant_names *[Symbol]
69+
# @return [Constant, Array<Constant>]
6470
def off_const(base_module, *constant_names)
6571
spies = constant_names.map do |constant_name|
6672
case constant_name
@@ -83,14 +89,16 @@ def teardown
8389
Agency.instance.dissolve!
8490
end
8591

86-
# (see Double#new)
92+
# returns a double
93+
# (see Double#initizalize)
8794
def double(*args)
8895
Double.new(*args)
8996
end
9097

9198
# retrieve the spy from an object
92-
# @params base_object
93-
# @method_names *[Symbol, Hash]
99+
# @param base_object
100+
# @param method_names *[Symbol]
101+
# @return [Subroutine, Array<Subroutine>]
94102
def get(base_object, *method_names)
95103
spies = method_names.map do |method_name|
96104
Subroutine.get(base_object, method_name)
@@ -99,6 +107,10 @@ def get(base_object, *method_names)
99107
spies.size > 1 ? spies : spies.first
100108
end
101109

110+
# retrieve the constant spies from an object
111+
# @param base_module
112+
# @param constant_names *[Symbol]
113+
# @return [Constant, Array<Constant>]
102114
def get_const(base_module, *constant_names)
103115
spies = constant_names.map do |method_name|
104116
Constant.get(base_module, constant_name)

lib/spy/dsl.rb

Lines changed: 0 additions & 7 deletions
This file was deleted.

lib/spy/subroutine.rb

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
module Spy
22
class Subroutine
3-
CallLog = Struct.new(:object, :args, :block)
3+
CallLog = Struct.new(:object, :args, :block, :result)
44
attr_reader :base_object, :method_name, :calls, :original_method, :opts
5+
6+
# set what object and method the spy should watch
7+
# @param object
8+
# @param method_name <Symbol>
59
def initialize(object, method_name)
610
@was_hooked = false
711
@base_object, @method_name = object, method_name
@@ -58,20 +62,32 @@ def hooked?
5862
self == self.class.get(base_object, method_name)
5963
end
6064

61-
# sets the return value of given spied method
62-
# @params return value
63-
# @params return block
65+
# @overload and_return(value)
66+
# @overload and_return(&block)
67+
#
68+
# Tells the spy to return a value when the method is called.
69+
#
6470
# @return self
65-
def and_return(value = nil, &block)
71+
def and_return(value = nil)
6672
if block_given?
67-
raise ArgumentError.new("value and block conflict. Choose one") if !value.nil?
68-
@plan = block
73+
@plan = Proc.new
74+
if value.nil? || value.is_a?(Hash) && value.has_key?(:force)
75+
if !(value.is_a?(Hash) && value[:force]) &&
76+
original_method &&
77+
original_method.arity >=0 &&
78+
@plan.arity > original_method.arity
79+
raise ArgumentError.new "The original method only has an arity of #{original_method.arity} you have an arity of #{@plan.arity}"
80+
end
81+
else
82+
raise ArgumentError.new("value and block conflict. Choose one") if !value.nil?
83+
end
6984
else
7085
@plan = Proc.new { value }
7186
end
7287
self
7388
end
7489

90+
# Tells the object to yield one or more args to a block when the message is received.
7591
def and_yield(*args)
7692
yield eval_context = Object.new if block_given?
7793
@plan = Proc.new do |&block|
@@ -124,9 +140,9 @@ def has_been_called_with?(*args)
124140
# method.
125141
def invoke(object, args, block)
126142
check_arity!(args.size)
127-
calls << CallLog.new(object, args, block)
128-
default_return_val = nil
129-
@plan ? @plan.call(*args, &block) : default_return_val
143+
result = @plan ? @plan.call(*args, &block) : nil
144+
calls << CallLog.new(object, args, block, result)
145+
result
130146
end
131147

132148
# reset the call log
@@ -139,7 +155,7 @@ def reset!
139155
private
140156

141157
def call_with_yield(&block)
142-
raise "no block sent" unless block
158+
raise "no block sent" unless block
143159
value = nil
144160
@args_to_yield.each do |args|
145161
if block.arity > -1 && args.length != block.arity

test/spy/test_subroutine.rb

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,14 +67,26 @@ def test_spy_and_return_returns_the_set_value
6767
def test_spy_and_return_can_call_a_block
6868
result = "hello world"
6969

70-
spy_on(@pen, :write).and_return do |string|
70+
spy_on(@pen, :write).and_return {}.and_return do |string|
7171
string.reverse
7272
end
7373

7474
assert_equal result.reverse, @pen.write(result)
7575
assert_empty @pen.written
7676
end
7777

78+
def test_spy_and_return_can_call_a_block_raises_when_there_is_an_arity_mismatch
79+
write_spy = spy_on(@pen, :write)
80+
write_spy.and_return do |*args|
81+
end
82+
write_spy.and_return do |string, *args|
83+
end
84+
assert_raises ArgumentError do
85+
write_spy.and_return do |string, b|
86+
end
87+
end
88+
end
89+
7890
def test_spy_and_return_can_call_a_block_that_recieves_a_block
7991
string = "hello world"
8092

0 commit comments

Comments
 (0)