spy 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. data/.gitignore +18 -0
  2. data/Gemfile +6 -0
  3. data/LICENSE.txt +22 -0
  4. data/README.md +133 -0
  5. data/Rakefile +8 -0
  6. data/TODO.md +8 -0
  7. data/lib/spy.rb +259 -0
  8. data/lib/spy/double.rb +11 -0
  9. data/lib/spy/dsl.rb +7 -0
  10. data/lib/spy/version.rb +3 -0
  11. data/spec/spec_helper.rb +39 -0
  12. data/spec/spy/and_call_original_spec.rb +152 -0
  13. data/spec/spy/and_yield_spec.rb +114 -0
  14. data/spec/spy/bug_report_10260_spec.rb +8 -0
  15. data/spec/spy/bug_report_10263_spec.rb +24 -0
  16. data/spec/spy/bug_report_496_spec.rb +18 -0
  17. data/spec/spy/bug_report_600_spec.rb +24 -0
  18. data/spec/spy/bug_report_7611_spec.rb +16 -0
  19. data/spec/spy/bug_report_8165_spec.rb +31 -0
  20. data/spec/spy/bug_report_830_spec.rb +21 -0
  21. data/spec/spy/bug_report_957_spec.rb +22 -0
  22. data/spec/spy/double_spec.rb +12 -0
  23. data/spec/spy/failing_argument_matchers_spec.rb +94 -0
  24. data/spec/spy/hash_excluding_matcher_spec.rb +67 -0
  25. data/spec/spy/hash_including_matcher_spec.rb +90 -0
  26. data/spec/spy/mock_spec.rb +734 -0
  27. data/spec/spy/multiple_return_value_spec.rb +119 -0
  28. data/spec/spy/mutate_const_spec.rb +481 -0
  29. data/spec/spy/nil_expectation_warning_spec.rb +56 -0
  30. data/spec/spy/null_object_mock_spec.rb +107 -0
  31. data/spec/spy/options_hash_spec.rb +35 -0
  32. data/spec/spy/partial_mock_spec.rb +196 -0
  33. data/spec/spy/passing_argument_matchers_spec.rb +142 -0
  34. data/spec/spy/precise_counts_spec.rb +68 -0
  35. data/spec/spy/serialization_spec.rb +110 -0
  36. data/spec/spy/stash_spec.rb +54 -0
  37. data/spec/spy/stub_implementation_spec.rb +62 -0
  38. data/spec/spy/stub_spec.rb +85 -0
  39. data/spec/spy/stubbed_message_expectations_spec.rb +47 -0
  40. data/spec/spy/test_double_spec.rb +57 -0
  41. data/spec/spy/to_ary_spec.rb +40 -0
  42. data/spy.gemspec +21 -0
  43. data/test/spy/test_double.rb +19 -0
  44. data/test/test_helper.rb +6 -0
  45. data/test/test_spy.rb +258 -0
  46. metadata +157 -0
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ *.sw*
4
+ .bundle
5
+ .config
6
+ .yardoc
7
+ Gemfile.lock
8
+ InstalledFiles
9
+ _yardoc
10
+ coverage
11
+ doc/
12
+ lib/bundler/man
13
+ pkg
14
+ rdoc
15
+ spec/reports
16
+ test/tmp
17
+ test/version_tmp
18
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in spies.gemspec
4
+ gemspec
5
+ gem 'pry'
6
+ gem 'pry-nav'
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Ryan Ong
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,133 @@
1
+ # Spy
2
+
3
+ Spy is a lightweight doubles framework that won't let your code mock your intelligence.
4
+
5
+ Inspired by the spy api of the jasmine javascript testing framework.
6
+
7
+ ## Why use this instead of rspec-mocks, mocha, or etc
8
+
9
+ * Raise error when you try to stub/spy a method that doesn't exist
10
+ * when you change your method name your unit tests will break
11
+ * Spy arity matches original method
12
+ * Your tests will raise an error if you use the wrong arity
13
+ * Spy visibility matches original method
14
+ * Your tests will raise an error if you try to call the method incorrectly
15
+ * Simple call log api
16
+ * easier to read tests
17
+ * less need to look at test framework documentation
18
+ * no expectations
19
+ * really who thought that was a good idea?
20
+ * absolutely no polution of global object space unless you want to
21
+
22
+ ## Installation
23
+
24
+ Add this line to your application's Gemfile:
25
+
26
+ gem 'spy'
27
+
28
+ And then execute:
29
+
30
+ $ bundle
31
+
32
+ Or install it yourself as:
33
+
34
+ $ gem install spy
35
+
36
+ ## Usage
37
+
38
+ ```ruby
39
+ class Person
40
+ def first_name
41
+ "John"
42
+ end
43
+
44
+ def last_name
45
+ "Smith"
46
+ end
47
+
48
+ def full_name
49
+ "#{first_name} #{last_name}"
50
+ end
51
+
52
+ def say(words)
53
+ puts words
54
+ end
55
+ end
56
+ ```
57
+
58
+ ### Standalone
59
+
60
+ ```ruby
61
+ person = Person.new
62
+
63
+ first_name_spy = Spy.on(person, :first_name)
64
+ person.first_name #=> nil
65
+ first_name_spy.called? #=> true
66
+
67
+ Spy.get(person, :first_name) #=> first_name_spy
68
+
69
+ Spy.off(person, :first_name)
70
+ person.first_name #=> "John"
71
+
72
+ first_name_spy.hook #=> first_name_spy
73
+ first_name_spy.and_return("Bob")
74
+ person.first_name #=> "Bob"
75
+
76
+ Spy.teardown
77
+ person.first_name #=> "John"
78
+
79
+ say_spy = Spy.on(person, :say)
80
+ person.say("hello") {
81
+ "everything accepts a block in ruby"
82
+ }
83
+ say_spy.say("world")
84
+
85
+ say_spy.called_with?("hello") #=> true
86
+ say_spy.calls.count #=> 1
87
+ say_spy.calls.first.args #=> ["hello"]
88
+ say_spy.calls.last.args #=> ["world"]
89
+
90
+ call_log = say_spy.calls.first
91
+ call_log.object #=> #<Person:0x00000000b2b858>
92
+ call_log.args #=> ["hello"]
93
+ call_log.block #=> #<Proc:0x00000000b1a9e0>
94
+ call_log.block.call #=> "everything accepts a block in ruby"
95
+ ```
96
+
97
+ ### MiniTest
98
+
99
+ ```ruby
100
+ require "spy"
101
+ MiniTest::TestCase.add_teardown_hook { Spy.teardown }
102
+ ```
103
+
104
+ ### Rspec
105
+
106
+ In spec\_helper.rb
107
+
108
+ ```ruby
109
+ require "rspec/autorun"
110
+ require "spy"
111
+ RSpec.configure do |c|
112
+ c.before { Spy.teardown }
113
+ end
114
+ ```
115
+
116
+ ### Test::Unit
117
+
118
+ ```ruby
119
+ require "spy"
120
+ class Test::Unit::TestCase
121
+ def setup
122
+ Spy.teardown
123
+ end
124
+ end
125
+ ```
126
+
127
+ ## Contributing
128
+
129
+ 1. Fork it
130
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
131
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
132
+ 4. Push to the branch (`git push origin my-new-feature`)
133
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rake/testtask'
3
+
4
+ Rake::TestTask.new do |t|
5
+ t.libs << "test"
6
+ t.test_files = FileList['test/**/test*.rb']
7
+ t.verbose = true
8
+ end
data/TODO.md ADDED
@@ -0,0 +1,8 @@
1
+ # Todo
2
+
3
+ * and_yield
4
+ * and_raise
5
+ * spy on CONSTANTS
6
+ * argument matchers
7
+ * count matchers
8
+ * watch all calls to an object
data/lib/spy.rb ADDED
@@ -0,0 +1,259 @@
1
+ require "spy/version"
2
+ require "spy/double"
3
+ require "spy/dsl"
4
+
5
+ class Spy
6
+ CallLog = Struct.new(:object, :args, :block)
7
+
8
+ attr_reader :base_object, :method_name, :calls, :original_method
9
+ def initialize(object, method_name)
10
+ @base_object, @method_name = object, method_name
11
+ reset!
12
+ end
13
+
14
+ # hooks the method into the object and stashes original method if it exists
15
+ # @param opts [Hash{force => false, visibility => nil}] set :force => true if you want it to ignore if the method exists, or visibility to [:public, :protected, :private] to overwride current visibility
16
+ # @return self
17
+ def hook(opts = {})
18
+ raise "#{method_name} method has already been hooked" if hooked?
19
+ raise "#{base_object} method '#{method_name}' has already been hooked" if self.class.get(base_object, method_name)
20
+ opts[:force] ||= base_object.is_a?(Double)
21
+ if base_object.respond_to?(method_name, true) || !opts[:force]
22
+ @original_method = base_object.method(method_name)
23
+ end
24
+
25
+ opts[:visibility] ||= method_visibility
26
+
27
+ if original_method && original_method.owner == base_object.singleton_class
28
+ base_object.singleton_class.send(:remove_method, method_name)
29
+ end
30
+
31
+ __method_spy__ = self
32
+ base_object.define_singleton_method(method_name) do |*__spy_args, &block|
33
+ if __spy_args.first === __method_spy__.class.__secret_method_key__
34
+ __method_spy__
35
+ else
36
+ __method_spy__.record(self,__spy_args,block)
37
+ end
38
+ end
39
+
40
+ base_object.singleton_class.send(opts[:visibility], method_name) if opts[:visibility]
41
+
42
+ @hooked = true
43
+ self
44
+ end
45
+
46
+ # unhooks method from object
47
+ # @return self
48
+ def unhook
49
+ raise "#{method_name} method has not been hooked" unless hooked?
50
+ base_object.singleton_class.send(:remove_method, method_name)
51
+ if original_method && original_method.owner == base_object.singleton_class
52
+ base_object.define_singleton_method(method_name, original_method)
53
+ base_object.singleton_class.send(method_visibility, method_name) if method_visibility
54
+ end
55
+ clear_method!
56
+ self
57
+ end
58
+
59
+ # is the spy hooked?
60
+ # @return Boolean
61
+ def hooked?
62
+ @hooked
63
+ end
64
+
65
+
66
+ # sets the return value of given spied method
67
+ # @params return value
68
+ # @params return block
69
+ # @return self
70
+ def and_return(value = nil, &block)
71
+ if block_given?
72
+ raise ArgumentError.new("value and block conflict. Choose one") if !value.nil?
73
+ @plan = block
74
+ else
75
+ @plan = Proc.new { value }
76
+ end
77
+ self
78
+ end
79
+
80
+ # tells the spy to call the original method
81
+ # @return self
82
+ def and_call_through
83
+ raise "can only call through if original method is set" unless method_visibility
84
+ if original_method
85
+ @plan = original_method
86
+ else
87
+ @plan = Proc.new do |*args, &block|
88
+ base_object.send(:method_missing, method_name, *args, &block)
89
+ end
90
+ end
91
+ self
92
+ end
93
+
94
+ def has_been_called?
95
+ calls.size > 0
96
+ end
97
+
98
+ # check if the method was called with the exact arguments
99
+ def has_been_called_with?(*args)
100
+ calls.any? do |call_log|
101
+ call_log.args == args
102
+ end
103
+ end
104
+
105
+ # record that the method has been called. You really shouldn't use this
106
+ # method.
107
+ def record(object, args, block)
108
+ check_arity!(args.size)
109
+ calls << CallLog.new(object, args, block)
110
+ @plan.call(*args, &block) if @plan
111
+ end
112
+
113
+ # reset the call log
114
+ def reset!
115
+ @calls = []
116
+ clear_method!
117
+ true
118
+ end
119
+
120
+ private
121
+
122
+ def clear_method!
123
+ @hooked = false
124
+ @original_method = @arity_range = @method_visibility = nil
125
+ end
126
+
127
+ def method_visibility
128
+ @method_visibility ||=
129
+ if base_object.respond_to?(method_name)
130
+ if original_method && original_method.owner.protected_method_defined?(method_name)
131
+ :protected
132
+ else
133
+ :public
134
+ end
135
+ elsif base_object.respond_to?(method_name, true)
136
+ :private
137
+ end
138
+ end
139
+
140
+ def check_arity!(arity)
141
+ return unless arity_range
142
+ if arity < arity_range.min
143
+ raise ArgumentError.new("wrong number of arguments (#{arity} for #{arity_range.min})")
144
+ elsif arity > arity_range.max
145
+ raise ArgumentError.new("wrong number of arguments (#{arity} for #{arity_range.max})")
146
+ end
147
+ end
148
+
149
+ def arity_range
150
+ @arity_range ||=
151
+ if original_method
152
+ min = max = 0
153
+ original_method.parameters.each do |type,_|
154
+ case type
155
+ when :req
156
+ min += 1
157
+ max += 1
158
+ when :opt
159
+ max += 1
160
+ when :rest
161
+ max = Float::INFINITY
162
+ end
163
+ end
164
+ (min..max)
165
+ end
166
+ end
167
+
168
+ class << self
169
+ # create a spy on given object
170
+ # @params base_object
171
+ # @params method_names *[Symbol] will spy on these methods
172
+ # @params method_names [Hash] will spy on these methods and also set default return values
173
+ # @return [Spy, Array<Spy>]
174
+ def on(base_object, *method_names)
175
+ spies = method_names.map do |method_name|
176
+ create_and_hook_spy(base_object, method_name)
177
+ end.flatten
178
+
179
+ spies.size > 1 ? spies : spies.first
180
+ end
181
+
182
+ # removes the spy from the
183
+ def off(base_object, *method_names)
184
+ removed_spies = method_names.map do |method_name|
185
+ unhook_and_remove_spy(base_object, method_name)
186
+ end.flatten
187
+
188
+ raise "No spies found" if removed_spies.empty?
189
+ removed_spies.size > 1 ? removed_spies : removed_spies.first
190
+ end
191
+
192
+ # get all hooked methods
193
+ # @return [Array<Spy>]
194
+ def all
195
+ @all ||= []
196
+ end
197
+
198
+ # unhook all methods
199
+ def teardown
200
+ all.each(&:unhook)
201
+ reset
202
+ end
203
+
204
+ # reset all hooked methods
205
+ def reset
206
+ @all = nil
207
+ end
208
+
209
+ # (see Double#new)
210
+ def double(*args)
211
+ Double.new(*args)
212
+ end
213
+
214
+ # @private
215
+ def __secret_method_key__
216
+ @__secret_method_key__ ||= Object.new
217
+ end
218
+
219
+ # retrieve the spy from an object
220
+ # @params base_object
221
+ # @method_names *[Symbol, Hash]
222
+ def get(base_object, *method_names)
223
+ spies = method_names.map do |method_name|
224
+ if base_object.singleton_methods.include?(method_name.to_sym) && base_object.method(method_name).parameters == [[:rest, :__spy_args], [:block, :block]]
225
+ base_object.send(method_name, __secret_method_key__)
226
+ end
227
+ end
228
+
229
+ spies.size > 1 ? spies : spies.first
230
+ end
231
+
232
+ private
233
+
234
+ def create_and_hook_spy(base_object, method_name, opts = {})
235
+ case method_name
236
+ when String, Symbol
237
+ spy = new(base_object, method_name).hook(opts)
238
+ all << spy
239
+ spy
240
+ when Hash
241
+ method_name.map do |name, result|
242
+ create_and_hook_spy(base_object, name, opts).and_return(result)
243
+ end
244
+ else
245
+ raise ArgumentError.new "#{method_name.class} is an invalid class, #on only accepts String, Symbol, and Hash"
246
+ end
247
+ end
248
+
249
+ def unhook_and_remove_spy(base_object, method_name)
250
+ removed_spies = []
251
+ all.delete_if do |spy|
252
+ if spy.base_object == base_object && spy.method_name == method_name
253
+ removed_spies << spy.unhook
254
+ end
255
+ end
256
+ removed_spies
257
+ end
258
+ end
259
+ end