grift 0.1.0 → 1.0.0

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.
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grift
4
+ class MockMethod
5
+ ##
6
+ # An Array wrapper that tracks the calls and results for a {Grift::MockMethod}.
7
+ #
8
+ class MockExecutions
9
+ ##
10
+ # A new instance of MockExecutions.
11
+ #
12
+ # @return [Grift::MockMethod::MockExectuions]
13
+ #
14
+ def initialize
15
+ @executions = []
16
+ end
17
+
18
+ ##
19
+ # Returns an array of the args used in each call to the mocked method
20
+ #
21
+ # @example
22
+ # my_mock = Grift.spy_on(Number, :+)
23
+ # x = (3 + 4) + 5
24
+ # my_mock.mock.calls
25
+ # #=> [[4], [5]]
26
+ #
27
+ # @return [Array<Array>] an array of arrays of args
28
+ #
29
+ def calls
30
+ @executions.map do |exec|
31
+ exec[:args]
32
+ end
33
+ end
34
+
35
+ ##
36
+ # Returns true if there have been no calls tracked.
37
+ #
38
+ # @example
39
+ # my_mock = Grift.mock(String, :upcase)
40
+ # my_mock.mock.empty?
41
+ # #=> true
42
+ # "apple".upcase
43
+ # #=> "APPLE"
44
+ # my_mock.mock.empty?
45
+ # #=> false
46
+ #
47
+ # @return [Boolean] if the executions are empty
48
+ #
49
+ def empty?
50
+ @executions.empty?
51
+ end
52
+
53
+ ##
54
+ # Returns the count of executions.
55
+ #
56
+ # @example
57
+ # my_mock = Grift.mock(String, :upcase)
58
+ # my_mock.mock.count
59
+ # #=> 0
60
+ # "apple".upcase
61
+ # #=> "APPLE"
62
+ # my_mock.mock.count
63
+ # #=> 1
64
+ #
65
+ # @return [Number] the number of executions
66
+ #
67
+ def count
68
+ @executions.count
69
+ end
70
+
71
+ ##
72
+ # Returns an array of the results of each call to the mocked method
73
+ #
74
+ # @example
75
+ # my_mock = Grift.spy_on(Number, :+)
76
+ # x = (3 + 4) + 5
77
+ # my_mock.mock.results
78
+ # #=> [7, 12]
79
+ #
80
+ # @return [Array] an array of results
81
+ #
82
+ def results
83
+ @executions.map do |exec|
84
+ exec[:result]
85
+ end
86
+ end
87
+
88
+ ##
89
+ # Stores an args and result pair to the executions array.
90
+ #
91
+ # @example
92
+ # mock_store = Grift::MockMethod::MockExecutions.new
93
+ # mock_store.store([1, 1], [2])
94
+ #
95
+ # @return [Array] an array of results
96
+ #
97
+ def store(args, result)
98
+ @executions.push({ args: args, result: result })
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,284 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grift
4
+ ##
5
+ # A mock for a given class and method. This is the core of Grift. Mocking or spying
6
+ # usually returns a {Grift::MockMethod}.
7
+ #
8
+ class MockMethod
9
+ attr_reader :true_method_cached, :klass, :method_name
10
+
11
+ CACHE_METHOD_PREFIX = 'grift_cache'
12
+ private_constant :CACHE_METHOD_PREFIX
13
+
14
+ ##
15
+ # A new instance of MockMethod. Should be initialized via {Grift.mock} or {Grift.spy_on}
16
+ #
17
+ # @see Grift.mock
18
+ # @see Grift.spy_on
19
+ #
20
+ # @example
21
+ # Grift.spy_on(MyClass, :my_method)
22
+ # #=> MockMethod instance with the method being watched
23
+ #
24
+ # @param klass [Class] the class to be mocked
25
+ # @param method_name [Symbol] the method to be mocked
26
+ # @param watch [Boolean] whether to start watching the method
27
+ #
28
+ # @return [Grift::MockMethod]
29
+ #
30
+ def initialize(klass, method_name, watch: true)
31
+ if Grift.restricted_method?(klass, method_name)
32
+ raise(Grift::Error, "Cannont mock restricted method #{method_name} for class #{klass}")
33
+ end
34
+
35
+ @klass = klass
36
+ @method_name = method_name
37
+ @true_method_cached = false
38
+ @mock_executions = MockExecutions.new
39
+ @cache_method_name = "#{CACHE_METHOD_PREFIX}_#{method_name}".to_sym
40
+
41
+ # class methods are really instance methods of the singleton class
42
+ @class_method = klass.singleton_class.instance_methods(true).include?(method_name)
43
+
44
+ unless class_instance.instance_methods(true).include?(method_name)
45
+ raise(Grift::Error, "Cannont mock unknown method #{method_name} for class #{klass}")
46
+ end
47
+
48
+ if class_instance.instance_methods.include?(@cache_method_name)
49
+ raise(Grift::Error, "Cannot mock already mocked method #{method_name} for class #{klass}")
50
+ end
51
+
52
+ watch_method if watch
53
+ end
54
+
55
+ ##
56
+ # Gets the data for the mock results and calls for this mock.
57
+ #
58
+ # @see Grift::MockMethod::MockExecutions#calls
59
+ # @see Grift::MockMethod::MockExecutions#results
60
+ #
61
+ # @example
62
+ # my_mock = Grift.spy_on(String, :upcase)
63
+ # "banana".upcase
64
+ # #=> 'BANANA'
65
+ # my_mock.mock.calls
66
+ # #=> [[]]
67
+ # my_mock.mock.results
68
+ # #=> [['BANANA']]
69
+ #
70
+ # @return [Grift::MockMethod::MockExecutions]
71
+ #
72
+ def mock
73
+ @mock_executions
74
+ end
75
+
76
+ ##
77
+ # Clears the mock execution and calls data for this mock, but
78
+ # keep the method mocked as before.
79
+ #
80
+ # @return [Grift::MockMethod::MockExecutions]
81
+ #
82
+ def mock_clear
83
+ @mock_executions = MockExecutions.new
84
+ end
85
+
86
+ ##
87
+ # Clears the mock execution and calls data for this mock, and
88
+ # mocks the method to return `nil`.
89
+ #
90
+ # @return [Grift::MockMethod::MockExecutions]
91
+ #
92
+ def mock_reset
93
+ executions = mock_clear
94
+ mock_return_value(nil)
95
+ executions
96
+ end
97
+
98
+ ##
99
+ # Clears the mock execution and calls data for this mock, and
100
+ # restores the method to its original behavior. By default it
101
+ # also stops watching the method. This cleans up the mocking
102
+ # and restores expected behavior.
103
+ #
104
+ # @param watch [Boolean] whether or not to keep watching the method
105
+ #
106
+ # @return [Grift::MockMethod::MockExecutions]
107
+ #
108
+ def mock_restore(watch: false)
109
+ executions = mock_clear
110
+ unmock_method if @true_method_cached
111
+ watch_method if watch
112
+ executions
113
+ end
114
+
115
+ ##
116
+ # Accepts a block and mocks the method to execute that block instead
117
+ # of the original behavior whenever called while mocked.
118
+ #
119
+ # @example
120
+ # my_mock = Grift.spy_on(String, :downcase).mock_implementation do
121
+ # x = 3 + 4
122
+ # x.to_s
123
+ # end
124
+ # "Banana".downcase
125
+ # #=> '7'
126
+ #
127
+ # @example
128
+ # my_mock = Grift.spy_on(MyClass, :my_method).mock_implementation do |first, second|
129
+ # [second, first]
130
+ # end
131
+ # MyClass.my_method(1, 2)
132
+ # #=> [2, 1]
133
+ #
134
+ # @return [Grift::MockMethod] the mock itself
135
+ #
136
+ def mock_implementation(&block)
137
+ premock_setup
138
+ mock_executions = @mock_executions # required to access inside class instance block
139
+
140
+ class_instance.remove_method(@method_name)
141
+ class_instance.define_method @method_name do |*args|
142
+ return_value = block.call(*args)
143
+
144
+ # record the args passed in the call to the method and the result
145
+ mock_executions.store(args, return_value)
146
+ return return_value
147
+ end
148
+
149
+ self
150
+ end
151
+
152
+ ##
153
+ # Accepts a value and mocks the method to return that value instead
154
+ # of executing its original behavior while mocked.
155
+ #
156
+ # @see Grift#mock
157
+ #
158
+ # @example
159
+ # my_mock = Grift.spy_on(String, :upcase).mock_return_value('BANANA')
160
+ # "apple".upcase
161
+ # #=> 'BANANA'
162
+ #
163
+ # @param return_value the value to return from the method
164
+ #
165
+ # @return [Grift::MockMethod] the mock itself
166
+ #
167
+ def mock_return_value(return_value = nil)
168
+ premock_setup
169
+ mock_executions = @mock_executions # required to access inside class instance block
170
+
171
+ class_instance.remove_method(@method_name)
172
+ class_instance.define_method @method_name do |*args|
173
+ # record the args passed in the call to the method and the result
174
+ mock_executions.store(args, return_value)
175
+ return return_value
176
+ end
177
+
178
+ self
179
+ end
180
+
181
+ ##
182
+ # String representation of the MockMethod
183
+ #
184
+ # @see Grift::MockMethod.hash_key
185
+ #
186
+ # @return [String]
187
+ #
188
+ def to_s
189
+ Grift::MockMethod.hash_key(@klass, @method_name)
190
+ end
191
+
192
+ ##
193
+ # Hashes the class and method for tracking mocks.
194
+ #
195
+ # @example
196
+ # Grift::MockMethod.hash_key(String, :upcase)
197
+ # #=> 'String#upcase'
198
+ #
199
+ # @param klass [Class]
200
+ # @param method_name [Symbol]
201
+ #
202
+ # @return [String] the hash of the class and method
203
+ #
204
+ def self.hash_key(klass, method_name)
205
+ "#{klass}\##{method_name}"
206
+ end
207
+
208
+ private
209
+
210
+ ##
211
+ # Watches the method without mocking its impelementation or return value.
212
+ #
213
+ # @return [Grift::MockMethod] the mock itself
214
+ #
215
+ def watch_method
216
+ premock_setup
217
+ mock_executions = @mock_executions # required to access inside class instance block
218
+ cache_method_name = @cache_method_name
219
+
220
+ class_instance.remove_method(@method_name)
221
+ class_instance.define_method @method_name do |*args|
222
+ return_value = send(cache_method_name, *args)
223
+
224
+ # record the args passed in the call to the method and the result
225
+ mock_executions.store(args, return_value)
226
+ return return_value
227
+ end
228
+
229
+ self
230
+ end
231
+
232
+ ##
233
+ # Unmocks the method and restores the true method
234
+ #
235
+ # @raise [Grift:Error] if method not mocked
236
+ #
237
+ def unmock_method
238
+ raise(Grift::Error, 'Method is not cached') unless @true_method_cached
239
+
240
+ class_instance.remove_method(@method_name)
241
+ class_instance.alias_method(@method_name, @cache_method_name)
242
+ class_instance.remove_method(@cache_method_name)
243
+
244
+ @true_method_cached = false
245
+ end
246
+
247
+ ##
248
+ # Caches the method
249
+ #
250
+ # @raise [Grift::Error] if method already cached
251
+ #
252
+ def cache_method
253
+ raise(Grift::Error, 'Method already cached') if @true_method_cached
254
+
255
+ class_instance.alias_method(@cache_method_name, @method_name)
256
+ @true_method_cached = true
257
+ end
258
+
259
+ ##
260
+ # Sets up mock actions by caching the method and storing it in the
261
+ # Grift global store.
262
+ #
263
+ def premock_setup
264
+ cache_method unless @true_method_cached
265
+ send_to_store
266
+ end
267
+
268
+ ##
269
+ # Adds the mock to the global store
270
+ #
271
+ def send_to_store
272
+ Grift.mock_store.store(self) unless Grift.mock_store.include?(self)
273
+ end
274
+
275
+ ##
276
+ # Returns the appropriate class instance
277
+ #
278
+ # @return [Class]
279
+ #
280
+ def class_instance
281
+ @class_method ? @klass.singleton_class : @klass
282
+ end
283
+ end
284
+ end
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grift
4
+ ##
5
+ # A hash wrapper that tracks what methods are mocked and facilitates cleanup of mocks.
6
+ # This class is meant for internal use and should generally not be called explcitly.
7
+ #
8
+ class MockStore
9
+ # A new instance of MockStore
10
+ # @return [Grift::MockStore]
11
+ def initialize
12
+ @mocks = {}
13
+ end
14
+
15
+ ##
16
+ # Add the given mock to the mock store.
17
+ #
18
+ # @example
19
+ # mock_store = Grift::MockStore.new
20
+ # mock_method = Grift.mock(MyClass, :my_method)
21
+ # mock_store.store(mock_method)
22
+ #
23
+ # @param mock_method [Grift::MockMethod] the mock method to add to the store
24
+ #
25
+ # @raise [Grift::Error] if mock_method is not of type {Grift::MockMethod}
26
+ # @raise [Grift::Error] if the store already contains that mock or an equivalent mock
27
+ #
28
+ # @return [Grift::MockMethod] the mock method that was added
29
+ #
30
+ def store(mock_method)
31
+ raise(Grift::Error, 'Must only store Grift Mocks') unless mock_method.instance_of?(Grift::MockMethod)
32
+ raise(Grift::Error, 'Store aready contains that mock') if include?(mock_method)
33
+
34
+ @mocks[mock_method.to_s] = mock_method
35
+ end
36
+
37
+ ##
38
+ # Searches for the mock in the store. Optional filtering by class/method.
39
+ # If no parameters are passed in, this returns all mocks in the store.
40
+ #
41
+ # @param klass [Class] the class to filter by, if nil all classes are included
42
+ # @param method [Symbol] the method to filter by, if nil all symbols are included
43
+ #
44
+ # @return [Array] the mocks in the store that match the criteria
45
+ #
46
+ def mocks(klass: nil, method: nil)
47
+ search(klass: klass, method: method).values
48
+ end
49
+
50
+ ##
51
+ # Unmocks and removes mocks in the store. Optional filtering by class/method.
52
+ # If no parameters are passed in, cleans up all mocks in the store.
53
+ #
54
+ # @param klass [Class] the class to filter by, if nil all classes are included
55
+ # @param method [Symbol] the method to filter by, if nil all symbols are included
56
+ #
57
+ # @return [Grift::MockStore] the updated mock store itself
58
+ #
59
+ def remove(klass: nil, method: nil)
60
+ to_remove = search(klass: klass, method: method)
61
+ to_remove.each do |key, mock|
62
+ mock.mock_restore(watch: false)
63
+ @mocks.delete(key)
64
+ end
65
+
66
+ self
67
+ end
68
+
69
+ ##
70
+ # Unmocks and removes the mock in the store. If the mock is not in the store,
71
+ # nothing will change.
72
+ #
73
+ # @param mock_method [Grift::MockMethod, String] the mock to remove or its hash key
74
+ #
75
+ # @return [Grift::MockStore] the updated mock store itself
76
+ #
77
+ def delete(mock_method)
78
+ if include?(mock_method)
79
+ mock_method.mock_restore(watch: false)
80
+ @mocks.delete(mock_method.to_s)
81
+ end
82
+
83
+ self
84
+ end
85
+
86
+ ##
87
+ # Checks if the mock store includes the given mock method.
88
+ #
89
+ # @example
90
+ # mock_store = Grift::MockStore.new
91
+ # mock_method = Grift.mock(MyClass, :my_method)
92
+ # mock_store.include?(mock_method)
93
+ # #=> false
94
+ # mock_store.store(mock_method)
95
+ # mock_store.include?(mock_method)
96
+ # #=> true
97
+ #
98
+ # @param mock_method [Grift::MockMethod, String] the mock to search for or its hash key
99
+ #
100
+ # @return [Boolean] if the mock store includes that mock method
101
+ #
102
+ def include?(mock_method)
103
+ @mocks.include?(mock_method.to_s)
104
+ end
105
+
106
+ ##
107
+ # Checks if the mock store is empty.
108
+ #
109
+ # @example
110
+ # mock_store = Grift::MockStore.new
111
+ # mock_store.empty?
112
+ # #=> true
113
+ # mock_method = Grift.mock(MyClass, :my_method)
114
+ # mock_store.store(mock_method)
115
+ # mock_store.empty?
116
+ # #=> false
117
+ #
118
+ # @return [Boolean] if the mock store is empty
119
+ #
120
+ def empty?
121
+ @mocks.empty?
122
+ end
123
+
124
+ private
125
+
126
+ ##
127
+ # Searches for the mock in the store. Optional filtering by class/method.
128
+ #
129
+ # @param klass [Class] the class to filter by, if nil all classes are included
130
+ # @param method [Symbol] the method to filter by, if nil all symbols are included
131
+ #
132
+ # @return [Hash] the key, mock pairs in the store that match the criteria
133
+ #
134
+ def search(klass: nil, method: nil)
135
+ return @mocks unless klass || method
136
+
137
+ pattern = Regexp.new(Grift::MockMethod.hash_key(klass, method))
138
+ @mocks.select { |k| pattern.match?(k) }
139
+ end
140
+ end
141
+ end
data/lib/grift/version.rb CHANGED
@@ -1,3 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Grift
2
- VERSION = "0.1.0"
4
+ # gem version
5
+ VERSION = '1.0.0'
6
+ public_constant :VERSION
3
7
  end