minispec 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 (83) hide show
  1. checksums.yaml +7 -0
  2. data/.pryrc +2 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE +22 -0
  5. data/README.md +2140 -0
  6. data/Rakefile +11 -0
  7. data/bin/minispec +4 -0
  8. data/lib/minispec.rb +175 -0
  9. data/lib/minispec/api.rb +2 -0
  10. data/lib/minispec/api/class.rb +195 -0
  11. data/lib/minispec/api/class/after.rb +49 -0
  12. data/lib/minispec/api/class/around.rb +54 -0
  13. data/lib/minispec/api/class/before.rb +101 -0
  14. data/lib/minispec/api/class/helpers.rb +116 -0
  15. data/lib/minispec/api/class/let.rb +44 -0
  16. data/lib/minispec/api/class/tests.rb +33 -0
  17. data/lib/minispec/api/instance.rb +158 -0
  18. data/lib/minispec/api/instance/mocks/doubles.rb +36 -0
  19. data/lib/minispec/api/instance/mocks/mocks.rb +319 -0
  20. data/lib/minispec/api/instance/mocks/spies.rb +17 -0
  21. data/lib/minispec/api/instance/mocks/stubs.rb +105 -0
  22. data/lib/minispec/helpers.rb +1 -0
  23. data/lib/minispec/helpers/array.rb +56 -0
  24. data/lib/minispec/helpers/booleans.rb +108 -0
  25. data/lib/minispec/helpers/generic.rb +24 -0
  26. data/lib/minispec/helpers/mocks/expectations.rb +29 -0
  27. data/lib/minispec/helpers/mocks/spies.rb +36 -0
  28. data/lib/minispec/helpers/raise.rb +44 -0
  29. data/lib/minispec/helpers/throw.rb +29 -0
  30. data/lib/minispec/mocks.rb +11 -0
  31. data/lib/minispec/mocks/expectations.rb +77 -0
  32. data/lib/minispec/mocks/stubs.rb +178 -0
  33. data/lib/minispec/mocks/validations.rb +80 -0
  34. data/lib/minispec/mocks/validations/amount.rb +63 -0
  35. data/lib/minispec/mocks/validations/arguments.rb +161 -0
  36. data/lib/minispec/mocks/validations/caller.rb +43 -0
  37. data/lib/minispec/mocks/validations/order.rb +47 -0
  38. data/lib/minispec/mocks/validations/raise.rb +111 -0
  39. data/lib/minispec/mocks/validations/return.rb +74 -0
  40. data/lib/minispec/mocks/validations/throw.rb +91 -0
  41. data/lib/minispec/mocks/validations/yield.rb +141 -0
  42. data/lib/minispec/proxy.rb +201 -0
  43. data/lib/minispec/reporter.rb +185 -0
  44. data/lib/minispec/utils.rb +139 -0
  45. data/lib/minispec/utils/differ.rb +325 -0
  46. data/lib/minispec/utils/pretty_print.rb +51 -0
  47. data/lib/minispec/utils/raise.rb +123 -0
  48. data/lib/minispec/utils/throw.rb +140 -0
  49. data/minispec.gemspec +27 -0
  50. data/test/mocks/expectations/amount.rb +67 -0
  51. data/test/mocks/expectations/arguments.rb +126 -0
  52. data/test/mocks/expectations/caller.rb +55 -0
  53. data/test/mocks/expectations/generic.rb +35 -0
  54. data/test/mocks/expectations/order.rb +46 -0
  55. data/test/mocks/expectations/raise.rb +166 -0
  56. data/test/mocks/expectations/return.rb +71 -0
  57. data/test/mocks/expectations/throw.rb +113 -0
  58. data/test/mocks/expectations/yield.rb +109 -0
  59. data/test/mocks/spies/amount.rb +68 -0
  60. data/test/mocks/spies/arguments.rb +57 -0
  61. data/test/mocks/spies/generic.rb +61 -0
  62. data/test/mocks/spies/order.rb +38 -0
  63. data/test/mocks/spies/raise.rb +158 -0
  64. data/test/mocks/spies/return.rb +71 -0
  65. data/test/mocks/spies/throw.rb +113 -0
  66. data/test/mocks/spies/yield.rb +101 -0
  67. data/test/mocks/test__doubles.rb +98 -0
  68. data/test/mocks/test__expectations.rb +27 -0
  69. data/test/mocks/test__mocks.rb +197 -0
  70. data/test/mocks/test__proxies.rb +61 -0
  71. data/test/mocks/test__spies.rb +43 -0
  72. data/test/mocks/test__stubs.rb +427 -0
  73. data/test/proxified_asserts.rb +34 -0
  74. data/test/setup.rb +53 -0
  75. data/test/test__around.rb +58 -0
  76. data/test/test__assert.rb +510 -0
  77. data/test/test__before_and_after.rb +117 -0
  78. data/test/test__before_and_after_all.rb +71 -0
  79. data/test/test__helpers.rb +197 -0
  80. data/test/test__raise.rb +104 -0
  81. data/test/test__skip.rb +41 -0
  82. data/test/test__throw.rb +103 -0
  83. metadata +196 -0
@@ -0,0 +1,36 @@
1
+ module MiniSpec
2
+ module InstanceAPI
3
+
4
+ # creates a double object.
5
+ # if one or more arguments given, first argument will be used as name, unless it is a Hash.
6
+ # arguments that goes after first one are treated as stubs.
7
+ #
8
+ # @example create a double that will respond to `color` and reported as :apple
9
+ # apple = double(:apple, :color) { 'Red' }
10
+ # apple.color # => Red
11
+ #
12
+ # @example injecting a double into a real battle and expecting it to receive some messages
13
+ # user = double(:user, :name, :address)
14
+ # expect(user).to_receive(:name, :address)
15
+ # Shipping.new.get_address_for(user)
16
+ #
17
+ # @example spy on a double
18
+ # user = double(:user, :name, :address)
19
+ # Shipping.new.get_address_for(user)
20
+ # assert(user).received(:name, :address)
21
+ #
22
+ def double *args, &proc
23
+ name = args.first.is_a?(Hash) ? nil : args.shift
24
+
25
+ object = Object.new
26
+ object.define_singleton_method(:__ms__double_instance) {true}
27
+ object.define_singleton_method(:inspect) {name} if name
28
+
29
+ hashes, rest = args.partition {|s| s.is_a?(Hash)}
30
+ hashes.each {|h| stub(object, h)}
31
+ rest.each {|s| stub(object, s, &proc)}
32
+
33
+ object
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,319 @@
1
+ module MiniSpec
2
+ module InstanceAPI
3
+
4
+ # the mock is basically a stub with difference it will also add a expectation.
5
+ # that's it, a mock will stub a method on a object and
6
+ # will expect that stub to be called before test finished.
7
+ #
8
+ # the `mock` method will return the actual stub
9
+ # so you can build chained constraints on it.
10
+ #
11
+ # @note if mocked method exists it's visibility will be kept
12
+ #
13
+ # @example make `some_object` to respond to `:some_method`
14
+ # and expect `:some_method` to be called before current test finished.
15
+ # also make `:some_method` to behave differently depending on given arguments.
16
+ # so if called with [:a, :b] arguments it will return 'called with a, b'.
17
+ # called with [:x, :y] arguments it will return 'called with x, y'.
18
+ # called with any other arguments or without arguments at all it returns 'whatever'.
19
+ #
20
+ # mock(some_object, :some_method).
21
+ # with(:a, :b) { 'called with a, b' }.
22
+ # with(:x, :y) { 'called with x, y' }.
23
+ # with_any { 'whatever' }
24
+ #
25
+ def mock object, method, visibility = nil, &proc
26
+ if method.is_a?(Hash)
27
+ proc && raise(ArgumentError, 'Both Hash and block given. Please use either one.')
28
+ method.each_pair {|m,r| mock(object, m, visibility, &proc {r})}
29
+ return MiniSpec::Mocks::HashedStub
30
+ end
31
+ visibility ||= MiniSpec::Utils.method_visibility(object, method) || :public
32
+ # IMPORTANT! stub should be defined before expectation
33
+ stub = stub(object, method, visibility, &proc)
34
+ expect(object).to_receive(method)
35
+ stub
36
+ end
37
+
38
+ # mocking multiple methods at once
39
+ #
40
+ # @param object
41
+ # @param *methods
42
+ # @param &proc
43
+ # @return MiniSpec::Mocks::MultipleStubsProxy instance
44
+ #
45
+ def mocks object, *methods, &proc
46
+ MiniSpec::Mocks::MultipleStubsProxy.new(methods.map {|m| mock(object, m, &proc)})
47
+ end
48
+
49
+ # same as `mock` except it will enforce public visibility on mocked method.
50
+ def public_mock object, method, &proc
51
+ mock(object, method, :public, &proc)
52
+ end
53
+
54
+ def public_mocks object, *methods, &proc
55
+ MiniSpec::Mocks::MultipleStubsProxy.new(methods.map {|m| public_mock(object, m, &proc)})
56
+ end
57
+
58
+ def protected_mock object, method, &proc
59
+ mock(object, method, :protected, &proc)
60
+ end
61
+
62
+ def protected_mocks object, *methods, &proc
63
+ MiniSpec::Mocks::MultipleStubsProxy.new(methods.map {|m| mock(object, m, :protected, &proc)})
64
+ end
65
+
66
+ def private_mock object, method, &proc
67
+ mock(object, method, :private, &proc)
68
+ end
69
+
70
+ def private_mocks object, *methods, &proc
71
+ MiniSpec::Mocks::MultipleStubsProxy.new(methods.map {|m| mock(object, m, :private, &proc)})
72
+ end
73
+
74
+ # overriding given method of given object with a proxy
75
+ # so MiniSpec can later check whether given method was called.
76
+ #
77
+ # if given method does not exists a NoMethodError raised
78
+ #
79
+ # @note doubles and stubs will be skipped as they are already proxified
80
+ #
81
+ # @example
82
+ # proxy(obj, :a)
83
+ # assert(obj).received(:a) # checking whether obj received :a message
84
+ #
85
+ # @param object
86
+ # @param method_name
87
+ def proxy object, method_name
88
+ # do not proxify doubles
89
+ return if object.respond_to?(:__ms__double_instance)
90
+
91
+ # do not proxify stubs
92
+ return if (x = @__ms__stubs__originals) && (x = x[object]) && x[method_name]
93
+
94
+ proxies = (@__ms__proxies[object] ||= [])
95
+ return if proxies.include?(method_name)
96
+ proxies << method_name
97
+
98
+ # method exists and it is a singleton.
99
+ # `nil?` method can be overridden only through a singleton
100
+ if method_name == :nil? || object.singleton_methods.include?(method_name)
101
+ return __ms__mocks__define_singleton_proxy(object, method_name)
102
+ end
103
+
104
+ # method exists and it is not a singleton, define a regular proxy
105
+ if visibility = MiniSpec::Utils.method_visibility(object, method_name)
106
+ return __ms__mocks__define_regular_proxy(object, method_name, visibility)
107
+ end
108
+
109
+ raise(NoMethodError, '%s does not respond to %s. Can not proxify an un-existing method.' % [
110
+ object.inspect, method_name.inspect
111
+ ])
112
+ end
113
+
114
+ # replaces given method with a proxy
115
+ # that collects received messages and calls the original method.
116
+ #
117
+ # @param object
118
+ # @param method_name
119
+ # @param visibility
120
+ def __ms__mocks__define_regular_proxy object, method_name, visibility
121
+ method = object.method(method_name).unbind
122
+ method = __ms__mocks__regular_proxy(object, method_name, method)
123
+ extender = Module.new do
124
+ define_method(method_name, &method)
125
+ private method_name if visibility == :private
126
+ protected method_name if visibility == :protected
127
+ end
128
+ object.extend(extender)
129
+ end
130
+
131
+ # defines a singleton proxy
132
+ # that collects received messages and calls the original method.
133
+ #
134
+ # @param object
135
+ # @param method_name
136
+ def __ms__mocks__define_singleton_proxy object, method_name
137
+ method = object.method(method_name).unbind
138
+ method = __ms__mocks__regular_proxy(object, method_name, method)
139
+ object.define_singleton_method(method_name, &method)
140
+ end
141
+
142
+ # defines a singleton proxy
143
+ # that collects received messages and calls nothing.
144
+ #
145
+ # @note registering methods added this way so they can be undefined after test run
146
+ #
147
+ # @param object
148
+ # @param method_name
149
+ def __ms__mocks__define_void_proxy object, method_name
150
+ (@__ms__stubs__originals[object] ||= {})[method_name] = []
151
+ method = __ms__mocks__regular_proxy(object, method_name)
152
+ object.define_singleton_method(method_name, &method)
153
+ end
154
+
155
+ # returns a proc to be used with `define_method`.
156
+ # the proc will collect received messages then will call original method, if any.
157
+ #
158
+ # messages are stored into `@__ms__messages` Array
159
+ # each single message looks like:
160
+ # {object: ..., method: ..., arguments: ..., returned: ..., raised: ..., yielded: ...}
161
+ # `:returned` key are filled if original method called and it does not throw nor raise.
162
+ # `:raised` key are filled if original method called and it raises an error.
163
+ # `:yielded` key are filled if original method called with a block that was yielded.
164
+ #
165
+ # @param object
166
+ # @param method_name
167
+ # @param method [UnboundMethod] original method, unbounded, to be called after stat collected.
168
+ # if `nil`, there are two scenarios:
169
+ # 1. if method name is `:nil?` it returns `self == nil` after stat collected
170
+ # 2. otherwise it simply returns after stat collected
171
+ def __ms__mocks__regular_proxy object, method_name, method = nil
172
+ method_name.is_a?(Symbol) || raise(ArgumentError, 'method name should be a Symbol')
173
+
174
+ if :method_missing == method_name
175
+ return __ms__mocks__method_missing_proxy(object, method)
176
+ end
177
+ messages = @__ms__messages
178
+ Proc.new do |*args, &block|
179
+ message = {
180
+ object: object,
181
+ method: method_name,
182
+ arguments: args,
183
+ caller: Array(caller)
184
+ }
185
+ messages.push(message)
186
+
187
+ return self == nil if method_name == :nil?
188
+ return unless method
189
+
190
+ proc = block ? Proc.new do |*a,&b|
191
+ message[:yielded] = a
192
+ block.call(*a,&b)
193
+ end : nil
194
+
195
+ begin
196
+ message[:returned] = method.bind(self).call(*args, &proc)
197
+ rescue Exception => e
198
+ message[:raised] = e
199
+ end
200
+ message.freeze
201
+ message[:raised] ? raise(message[:raised]) : message[:returned]
202
+ end
203
+ end
204
+
205
+ # replace `method_missing` method with a proxy that collects
206
+ # received messages and calls original `method_missing` method.
207
+ # stat are collected for two methods:
208
+ # 1. `method_missing` itself
209
+ # 2. method what `method_missing` received as first argument
210
+ #
211
+ # stat has same format as on `__ms__mocks__regular_proxy`
212
+ # @see (#__ms__mocks__regular_proxy)
213
+ #
214
+ # @param object
215
+ # @param method [UnboundMethod] original `method_missing` method, unbounded
216
+ def __ms__mocks__method_missing_proxy object, method
217
+ messages = @__ms__messages
218
+ Proc.new do |meth, *args, &block|
219
+ message = {
220
+ object: object,
221
+ method: :method_missing,
222
+ arguments: [meth, *args],
223
+ caller: Array(caller)
224
+ }
225
+ messages.push(message)
226
+
227
+ message = {object: object, method: meth, arguments: args}
228
+ messages.push(message)
229
+
230
+ proc = block ? Proc.new do |*a,&b|
231
+ message[:yielded] = a
232
+ block.call(*a,&b)
233
+ end : nil
234
+
235
+ begin
236
+ message[:returned] = method.bind(self).call(meth, *args, &proc)
237
+ rescue Exception => e
238
+ message[:raised] = e
239
+ end
240
+ message.freeze
241
+ message[:raised] ? raise(message[:raised]) : message[:returned]
242
+ end
243
+ end
244
+
245
+ # restoring stubbed methods.
246
+ #
247
+ # it processes `@__ms__stubs__originals` Hash where keys are the objects
248
+ # and values are the object's methods to be restored.
249
+ # each value is a Array where first element is the method name
250
+ # and the second element is what previous method was.
251
+ # - if second element is an empty Array
252
+ # that mean method were not defined before stubbing, so simply undefine it.
253
+ # - if second element is a Array with last element set to :singleton Symbol,
254
+ # the method was a singleton before stubbing it,
255
+ # so defining a singleton method using second element's first element.
256
+ # - if second element is a Array with last element set to
257
+ # any of :public, :protected, :private Symbol
258
+ # an method with according visibility will be defined
259
+ # using second element's first element.
260
+ #
261
+ # @example there was no `x` method before stubbing
262
+ #
263
+ # # => {#<Object:0x007f92bb2b52c8>=>{:x=>[]}}
264
+ #
265
+ # @example `a` method was a singleton before stubbing
266
+ #
267
+ # # => {#<Object:0x007f92bb2c5998>=>{:a=>[#<Method: #<Object:0x007f92bb2c5998>.a>, :singleton]}}
268
+ #
269
+ # @example `a` was a public method before stubbing
270
+ #
271
+ # # => {#<#<Class:0x007f92bb2cdbe8>:0x007f92bb2cd850>=>{:a=>[#<Method: #<Class:0x007f92bb2cdbe8>#a>, :public]}}
272
+ #
273
+ def __ms__mocks__restore_originals
274
+ return unless stubs = @__ms__stubs__originals
275
+ stubs.each_pair do |object, methods|
276
+ methods.each_pair do |method_name, method|
277
+
278
+ # clearing proxies cache so the method can be proxied again during current test
279
+ (x = @__ms__proxies[object]) && x.delete(method_name)
280
+
281
+ if method.last.nil?
282
+ MiniSpec::Utils.undefine_method(object, method_name)
283
+ elsif method.last == :singleton
284
+ object.define_singleton_method(method_name, &method.first)
285
+ else
286
+ extender = Module.new do
287
+ define_method(method_name, &method.first)
288
+ private method_name if method.last == :private
289
+ protected method_name if method.last == :protected
290
+ end
291
+ object.extend(extender)
292
+ end
293
+
294
+ end
295
+ end
296
+ # clearing cache for cases when this run during current test
297
+ stubs.clear
298
+ end
299
+
300
+ # it is critical to iterate over a "statical" copy of messages array,
301
+ # otherwise iteration will generate a uncatchable infinite loop
302
+ # when messages array are updated during iteration.
303
+ def __ms__mocks__messages_copy
304
+ @__ms__messages.dup
305
+ end
306
+
307
+ # takes a copy of received messages and returns
308
+ # only messages received by given object
309
+ #
310
+ # @param object
311
+ def __ms__mocks__instance_messages object
312
+ __ms__mocks__messages_copy.select {|m| m[:object] == object}.freeze
313
+ end
314
+
315
+ def __ms__mocks__validate_expectations
316
+ catch(:__ms__stop_evaluation) { @__ms__expectations.each(&:validate!) }
317
+ end
318
+ end
319
+ end
@@ -0,0 +1,17 @@
1
+ module MiniSpec
2
+ module InstanceAPI
3
+
4
+ # basically by proxying an object we attach a spy on it
5
+ # so any received messages will be reported
6
+ #
7
+ # @example spying user for :login and :logout messages
8
+ # user = User.new
9
+ # spy(user, :login, :logout)
10
+ # # ...
11
+ # assert(user).received(:login, :logout)
12
+ #
13
+ def spy object, *methods
14
+ methods.each {|method| proxy(object, method)}
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,105 @@
1
+ module MiniSpec
2
+ module InstanceAPI
3
+
4
+ # stubbing given method and keeps original visibility
5
+ #
6
+ # if a block given, it will receive original method as first argument
7
+ # and any passed parameters as rest arguments.
8
+ #
9
+ # @example make Some::Remote::API.call to return success
10
+ # stub(Some::Remote::API, :call) { :success }
11
+ #
12
+ # @example call original
13
+ # stub(obj, :a) {|orig| orig.call}
14
+ #
15
+ # @param object object to define stub on
16
+ # @param stub method to be stubbed.
17
+ # if a Hash given, keys will be stubbed methods and values return values
18
+ # @param [Proc] &proc block to be yielded when stub called
19
+ # @return MiniSpec::Mocks::Stub instance
20
+ #
21
+ def stub object, stub, visibility = nil, &proc
22
+ [Symbol, String, Hash].include?(stub.class) ||
23
+ raise(ArgumentError, 'a Symbol, String or Hash expected')
24
+
25
+ if stub.is_a?(Hash)
26
+ return hash_stub(object, stub, visibility, &proc)
27
+ elsif stub =~ /\./
28
+ return chained_stub(object, stub, visibility, &proc)
29
+ end
30
+
31
+ visibility ||= MiniSpec::Utils.method_visibility(object, stub) || :public
32
+ stubs = (@__ms__stubs[object.__id__] ||= {})
33
+ stubs[stub] ||= MiniSpec::Mocks::Stub.new(object, @__ms__messages, @__ms__stubs__originals)
34
+ stubs[stub].stubify(stub, visibility, &proc)
35
+ stubs[stub]
36
+ end
37
+
38
+ # @example make `obj.a` to return :x and `obj.b` to return :y
39
+ # stub(obj, :a => :x, :b => :y)
40
+ #
41
+ def hash_stub object, hash, visibility = nil, &proc
42
+ proc && raise(ArgumentError, 'Both Hash and block given. Please use either one.')
43
+ hash.each_pair do |s,v|
44
+ stub(object, s, visibility, &proc {v})
45
+ end
46
+ return MiniSpec::Mocks::HashedStub
47
+ end
48
+
49
+ # @example define a chained stub
50
+ # stub(obj, 'a.b.c')
51
+ #
52
+ def chained_stub object, chain, visibility = nil, &block
53
+ chain = chain.split('.').map(&:to_sym)
54
+ base, last_index = self, chain.size - 1
55
+ chain.each_with_index do |m,i|
56
+ next_object = (i == last_index ? nil : Struct.new(chain[i+1]).new)
57
+ return stub(object, m, visibility, &block) unless next_object
58
+ stub(object, m, visibility) { next_object }
59
+ object = next_object
60
+ end
61
+ end
62
+
63
+ # same as `stub` except it defines multiple stubs at once
64
+ #
65
+ # @param object
66
+ # @param *stubs
67
+ # @param &proc
68
+ # @return MiniSpec::Mocks::MultipleStubsProxy instance
69
+ #
70
+ def stubs object, *stubs, &proc
71
+ MiniSpec::Mocks::MultipleStubsProxy.new(stubs.map {|s| stub(object, s, &proc)})
72
+ end
73
+
74
+ # stubbing a method and enforce public visibility on it.
75
+ # that's it, even if method exists and it is not public,
76
+ # after stubbing it will become public.
77
+ def public_stub object, stub, &proc
78
+ stub(object, stub, :public, &proc)
79
+ end
80
+
81
+ def public_stubs object, *stubs, &proc
82
+ MiniSpec::Mocks::MultipleStubsProxy.new(stubs.map {|s| public_stub(object, s, &proc)})
83
+ end
84
+
85
+ # same as stub except it defines protected stubs
86
+ # (@see #stub)
87
+ def protected_stub object, stub, &proc
88
+ stub(object, stub, :protected, &proc)
89
+ end
90
+
91
+ def protected_stubs object, *stubs, &proc
92
+ MiniSpec::Mocks::MultipleStubsProxy.new(stubs.map {|s| protected_stub(object, s, &proc)})
93
+ end
94
+
95
+ # same as stub except it defines private stubs
96
+ # (@see #stub)
97
+ def private_stub object, stub, &proc
98
+ stub(object, stub, :private, &proc)
99
+ end
100
+
101
+ def private_stubs object, *stubs, &proc
102
+ MiniSpec::Mocks::MultipleStubsProxy.new(stubs.map {|s| private_stub(object, s, &proc)})
103
+ end
104
+ end
105
+ end