surrogate 0.6.2 → 0.6.3

Sign up to get free protection for your applications and to get access to all the features.
data/Changelog.md CHANGED
@@ -1,3 +1,10 @@
1
+ ### 0.6.3
2
+
3
+ * Allow mass initialization of instances with `MockClass.factory key: value`
4
+ * The `.factory` method can be turned off with `Surrogate.endow self, factory: false`
5
+ * The `.factory` method can be renamed with `Surrogate.endow self, factory: :custom_factory_method_name`
6
+ * Allow mass initialization of clones with `MockClass.clone key: value`
7
+
1
8
  ### 0.6.2
2
9
 
3
10
  * Make substitutability matcher go either way (you should now do `RealClass.should substitute_for SurrogateClass` eventually, doing it in the other direction will not be supported)
data/Readme.md CHANGED
@@ -161,6 +161,8 @@ user.id # => 12
161
161
  ```
162
162
 
163
163
  Use **clone** to avoid altering state on the class.
164
+ You can pass it key/value pairs to mass initialize the clone
165
+ (I think it's probably best to avoid this kind of state on classes, but do support it)
164
166
  ```ruby
165
167
  class MockUser
166
168
  Surrogate.endow self do
@@ -168,12 +170,43 @@ class MockUser
168
170
  end
169
171
  end
170
172
 
173
+ # using a clone
171
174
  user_class = MockUser.clone
172
175
  user_class.find
173
176
  user_class.was told_to :find
174
177
  MockUser.was_not told_to :find
178
+
179
+ # initializing the clone
180
+ MockUser.clone(find: nil).find.should be_nil
175
181
  ```
176
182
 
183
+ **Mass initialize** with `.factory(key: value)`, this can be turned off by passing
184
+ `factory: false` as an option to Surrogate.endow, or given a different name if
185
+ the value is the new name.
186
+
187
+ ```ruby
188
+ # default factory method
189
+ class MockUserWithFactory
190
+ Surrogate.endow self
191
+ define(:name) { 'Samantha' }
192
+ define(:age) { 83 }
193
+ end
194
+
195
+ user = MockUserWithFactory.factory name: 'Jim', age: 26
196
+ user.name # => "Jim"
197
+ user.age # => 26
198
+
199
+ # use a different name
200
+ class MockUserWithoutFactory
201
+ Surrogate.endow self, factory: :construct
202
+ define(:name) { 'Samantha' }
203
+ define(:age) { 83 }
204
+ end
205
+
206
+ user = MockUserWithoutFactory.construct name: 'Milla'
207
+ user.name # => "Milla"
208
+ user.age # => 83
209
+ ```
177
210
 
178
211
 
179
212
  RSpec Integration
@@ -191,6 +191,8 @@ user.id # => 12
191
191
  ```
192
192
 
193
193
  Use **clone** to avoid altering state on the class.
194
+ You can pass it key/value pairs to mass initialize the clone
195
+ (I think it's probably best to avoid this kind of state on classes, but do support it)
194
196
  ```ruby
195
197
  <% test 'clone to avoid mutating state', with: :rspec, context: 'generic it block' do %>
196
198
  class MockUser
@@ -199,13 +201,46 @@ class MockUser
199
201
  end
200
202
  end
201
203
 
204
+ # using a clone
202
205
  user_class = MockUser.clone
203
206
  user_class.find
204
207
  user_class.was told_to :find
205
208
  MockUser.was_not told_to :find
209
+
210
+ # initializing the clone
211
+ MockUser.clone(find: nil).find.should be_nil
206
212
  <% end %>
207
213
  ```
208
214
 
215
+ **Mass initialize** with `.factory(key: value)`, this can be turned off by passing
216
+ `factory: false` as an option to Surrogate.endow, or given a different name if
217
+ the value is the new name.
218
+
219
+ ```ruby
220
+ <% test 'factory method', with: :magic_comments do %>
221
+ # default factory method
222
+ class MockUserWithFactory
223
+ Surrogate.endow self
224
+ define(:name) { 'Samantha' }
225
+ define(:age) { 83 }
226
+ end
227
+
228
+ user = MockUserWithFactory.factory name: 'Jim', age: 26
229
+ user.name # => "Jim"
230
+ user.age # => 26
231
+
232
+ # use a different name
233
+ class MockUserWithoutFactory
234
+ Surrogate.endow self, factory: :construct
235
+ define(:name) { 'Samantha' }
236
+ define(:age) { 83 }
237
+ end
238
+
239
+ user = MockUserWithoutFactory.construct name: 'Milla'
240
+ user.name # => "Milla"
241
+ user.age # => 83
242
+ <% end %>
243
+ ```
209
244
 
210
245
 
211
246
  RSpec Integration
@@ -62,12 +62,22 @@ class Surrogate
62
62
 
63
63
  # there is a lot of duplication in these next four methods -.-
64
64
  # idk if there is something we can do about it.
65
+ #
66
+ # The class methods below ignore the .clone method, b/c it's interface is intentionally different.
67
+ # This is so stupid, it needs to get smarter, it needs a way for the Surrogate or the user to tell it
68
+ # what to care about, it shouldn't have to know about what the endower is doing.
69
+ #
70
+ # In general, this code is just really fucking bad. I'd like to create some sort of method object
71
+ # that can encapsulate all of this information (mthod name, param names, types, existence)
72
+ # but I can't quite see it yet, and I'm scared to refactor to it since this code is tested primarily
73
+ # through unit tests (this is one of the few places I resorted to unit tests, and I'm so unhappy that
74
+ # I did).
65
75
 
66
76
  # types are only shown for methods on both objects
67
77
  def class_types
68
78
  class_methods_that_should_match.each_with_object Hash.new do |name, hash|
69
79
  surrogate_type, actual_type = class_types_for name
70
- next if surrogate_type == actual_type
80
+ next if surrogate_type == actual_type || name == :clone # ugh :(
71
81
  hash[name] = { surrogate: surrogate_type, actual: actual_type }
72
82
  end
73
83
  end
@@ -85,7 +95,7 @@ class Surrogate
85
95
  def class_names
86
96
  class_methods_that_should_match.each_with_object Hash.new do |method_name, hash|
87
97
  surrogate_name, actual_name = class_parameter_names_for method_name
88
- next if surrogate_name == actual_name
98
+ next if surrogate_name == actual_name || method_name == :clone # ugh :(
89
99
  hash[method_name] = { surrogate: surrogate_name, actual: actual_name }
90
100
  end
91
101
  end
@@ -5,6 +5,14 @@ class Surrogate
5
5
  # please refactor me! ...may not be possible :(
6
6
  # Can we move all method definitions into this class?
7
7
  class Endower
8
+ def self.uninitialized_instance_for(surrogate_class)
9
+ instance = surrogate_class.allocate
10
+ hatchery = surrogate_class.instance_variable_get :@hatchery
11
+ surrogate_class.last_instance = instance
12
+ instance.instance_variable_set :@hatchling, Hatchling.new(instance, hatchery)
13
+ instance
14
+ end
15
+
8
16
  def self.add_hook(&block)
9
17
  hooks << block
10
18
  end
@@ -13,14 +21,14 @@ class Surrogate
13
21
  @hooks ||= []
14
22
  end
15
23
 
16
- def self.endow(klass, &block)
17
- new(klass, &block).endow
24
+ def self.endow(klass, options, &block)
25
+ new(klass, options, &block).endow
18
26
  end
19
27
 
20
- attr_accessor :klass, :block
28
+ attr_accessor :klass, :block, :options
21
29
 
22
- def initialize(klass, &block)
23
- self.klass, self.block = klass, block
30
+ def initialize(klass, options, &block)
31
+ self.klass, self.options, self.block = klass, options, block
24
32
  end
25
33
 
26
34
  def endow
@@ -32,8 +40,9 @@ class Surrogate
32
40
 
33
41
  def endow_klass
34
42
  klass.extend ClassMethods
35
- add_hatchery_to klass
43
+ add_hatchery_to klass, options # do we want to pick out which options to pass?
36
44
  enable_defining_methods klass
45
+ enable_factory klass, options.fetch(:factory, :factory)
37
46
  klass.send :include, InstanceMethods
38
47
  enable_generic_override klass
39
48
  invoke_hooks klass
@@ -63,8 +72,8 @@ class Surrogate
63
72
  klass.singleton_class
64
73
  end
65
74
 
66
- def add_hatchery_to(klass)
67
- klass.instance_variable_set :@hatchery, Surrogate::Hatchery.new(klass)
75
+ def add_hatchery_to(klass, options={})
76
+ klass.instance_variable_set :@hatchery, Surrogate::Hatchery.new(klass, options)
68
77
  end
69
78
 
70
79
  def enable_defining_methods(klass)
@@ -72,6 +81,19 @@ class Surrogate
72
81
  @hatchery.define method_name, options, &block
73
82
  end
74
83
  end
84
+
85
+ def enable_factory(klass, factory_name)
86
+ return unless factory_name
87
+ klass.define_singleton_method factory_name do |overrides={}|
88
+ instance = begin
89
+ new
90
+ rescue ArgumentError
91
+ Endower.uninitialized_instance_for self
92
+ end
93
+ overrides.each { |attribute, value| instance.will_override attribute, value }
94
+ instance
95
+ end
96
+ end
75
97
  end
76
98
 
77
99
 
@@ -80,22 +102,21 @@ class Surrogate
80
102
 
81
103
  # Should this be dup? (dup seems to copy singleton methods) and may be able to use #initialize_copy to reset ivars
82
104
  # Can we just remove this feature an instead provide a reset feature which could be hooked into in before/after blocks (e.g. https://github.com/rspec/rspec-core/blob/622505d616d950ed53d12c6e82dbb953ba6241b4/lib/rspec/core/mocking/with_rspec.rb)
83
- def clone
105
+ def clone(overrides={})
84
106
  hatchling, hatchery, parent_name = @hatchling, @hatchery, name
85
107
  Class.new self do
86
108
  extend Module.new { define_method(:name) { parent_name && parent_name + '.clone' } } # inherit the name -- use module so that ApiComparison comes out correct (real classes inherit their name method)
87
- Surrogate.endow self do
109
+ Surrogate.endow self, hatchery.options do
88
110
  hatchling.api_methods.each { |name, options| define name, options.to_hash, &options.default_proc }
89
111
  end
90
112
  hatchery.api_methods.each { |name, options| define name, options.to_hash, &options.default_proc }
113
+ overrides.each { |attribute, value| @hatchling.prepare_method attribute, [value] }
91
114
  end
92
115
  end
93
116
 
94
117
  # Custom new, because user can define initialize, and we need to record it
95
118
  def new(*args)
96
- instance = allocate
97
- self.last_instance = instance
98
- instance.instance_variable_set :@hatchling, Hatchling.new(instance, @hatchery)
119
+ instance = Endower.uninitialized_instance_for self
99
120
  instance.__send__ :initialize, *args
100
121
  instance
101
122
  end
@@ -4,10 +4,14 @@ class Surrogate
4
4
  # The hatchlings are added to the instances, and they look here
5
5
  # to find out about how their methods are implemented.
6
6
  class Hatchery
7
- attr_accessor :klass
8
7
 
9
- def initialize(klass)
10
- self.klass = klass
8
+ # currently options don't do anything,
9
+ # they're just there because the hatchery is the definition of the surrogate
10
+ # so it makes sense for it to store values used by the endower when constructing clones
11
+ attr_accessor :klass, :options
12
+
13
+ def initialize(klass, options)
14
+ self.klass, self.options = klass, options
11
15
  klass_can_define_api_methods
12
16
  end
13
17
 
@@ -1,3 +1,3 @@
1
1
  class Surrogate
2
- VERSION = "0.6.2"
2
+ VERSION = "0.6.3"
3
3
  end
data/lib/surrogate.rb CHANGED
@@ -10,8 +10,8 @@ require 'surrogate/invocation'
10
10
  class Surrogate
11
11
  UnpreparedMethodError = Class.new StandardError
12
12
 
13
- def self.endow(klass, &block)
14
- Endower.endow klass, &block
13
+ def self.endow(klass, options={}, &block)
14
+ Endower.endow klass, options, &block
15
15
  klass
16
16
  end
17
17
  end
@@ -292,6 +292,17 @@ describe 'define' do
292
292
  superclass.clone.new.should be_a_kind_of superclass
293
293
  end
294
294
 
295
+ it 'can be mass initialized by passing a hash of attributes and values when cloning' do
296
+ pristine_klass = Surrogate.endow(Class.new) do
297
+ define(:find) { |n| 123 }
298
+ define(:bind) { 'abc' }
299
+ end
300
+ pristine_klass.clone.find(1).should == 123
301
+ pristine_klass.clone(find: 456, bind: 'def').find(1).should == 456
302
+ pristine_klass.clone.bind.should == 'abc'
303
+ pristine_klass.clone(find: 456, bind: 'def').bind.should == 'def'
304
+ end
305
+
295
306
  describe '.name' do
296
307
  it 'is nil for anonymous classes' do
297
308
  Surrogate.endow(Class.new).clone.name.should be_nil
@@ -183,3 +183,56 @@ describe 'inspect methods' do
183
183
  end
184
184
  end
185
185
  end
186
+
187
+ describe '.factory' do
188
+ it 'allows you to initialize the values of the returned mock' do
189
+ klass = Surrogate.endow(Class.new).define(:a){'a'}.define(:b){'b'}
190
+ klass.factory(a: 'A').a.should == 'A'
191
+ klass.factory(a: 'A').b.should == 'b'
192
+ klass.new.a.should == 'a'
193
+ end
194
+
195
+ it 'can be turned off at the class level with Surrogate.endow self factory: false' do
196
+ klass = Surrogate.endow(Class.new, factory: false).define(:a) { 'a' }
197
+ expect { klass.factory a: 'A' }.to raise_error NoMethodError
198
+ end
199
+
200
+ it 'can be given a different method name by passing the name to the endower' do
201
+ klass = Surrogate.endow(Class.new, factory: :construct).define(:a) { 'a' }
202
+ expect { klass.factory a: 'A' }.to raise_error NoMethodError
203
+ klass.construct(a: 'A').a.should == 'A'
204
+ end
205
+
206
+ it 'retains its setting on clones' do
207
+ clone = Surrogate.endow(Class.new, factory: :construct).define(:a){'a'}.clone
208
+ expect { clone.factory a: 'A' }.to raise_error NoMethodError
209
+ clone.construct(a: 'A').a.should == 'A'
210
+ clone.new.a.should == 'a'
211
+ end
212
+
213
+ context 'when the initialize method can be invoked without args' do
214
+ let(:klass) { Surrogate.endow(Class.new).define(:initialize) { } }
215
+ let!(:instance) { klass.factory }
216
+
217
+ it 'is invoked' do
218
+ instance.was initialized_with no_args
219
+ end
220
+
221
+ specify 'the instance is recorded as the last instance' do
222
+ klass.last_instance.should == instance
223
+ end
224
+ end
225
+
226
+ context 'when the initialize method cannot be invoked without args' do
227
+ let(:klass) { Surrogate.endow(Class.new).define(:initialize) {|arg|} }
228
+ let!(:instance) { klass.factory }
229
+
230
+ it 'is NOT invoked' do
231
+ instance.was_not told_to :initialize
232
+ end
233
+
234
+ specify 'the instance is recorded as the last instance' do
235
+ klass.last_instance.should == instance
236
+ end
237
+ end
238
+ end
data/todo CHANGED
@@ -10,6 +10,7 @@ Urgent (things I want to do immediately, formatted as the git commits I will use
10
10
  TODO (next up after urgent, these will happen whenever I get around to it)
11
11
  --------------------------------------------------------------------------
12
12
 
13
+ * when trying to set a return value, error is NoMethodError will_have_whatever, but would be nice for it to say `UndefinedMethodError bad_stuff is not defined`
13
14
  * Eventually it should be smart enough to not ignore natively implemented code with a [[:req]] argument
14
15
  * Remove dependency on all of RSpec and only depend on rspec-core, then have AC tests for the other shit
15
16
  * Add a better explanation for motivations
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: surrogate
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.2
4
+ version: 0.6.3
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-10-30 00:00:00.000000000 Z
12
+ date: 2012-10-31 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rake
16
- requirement: &70208657501360 !ruby/object:Gem::Requirement
16
+ requirement: &70232941137100 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: '0'
22
22
  type: :development
23
23
  prerelease: false
24
- version_requirements: *70208657501360
24
+ version_requirements: *70232941137100
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: rspec
27
- requirement: &70208657500800 !ruby/object:Gem::Requirement
27
+ requirement: &70232941136540 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ~>
@@ -32,10 +32,10 @@ dependencies:
32
32
  version: '2.4'
33
33
  type: :development
34
34
  prerelease: false
35
- version_requirements: *70208657500800
35
+ version_requirements: *70232941136540
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: mountain_berry_fields
38
- requirement: &70208657500220 !ruby/object:Gem::Requirement
38
+ requirement: &70232941135960 !ruby/object:Gem::Requirement
39
39
  none: false
40
40
  requirements:
41
41
  - - ~>
@@ -43,10 +43,10 @@ dependencies:
43
43
  version: 1.0.3
44
44
  type: :development
45
45
  prerelease: false
46
- version_requirements: *70208657500220
46
+ version_requirements: *70232941135960
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: mountain_berry_fields-rspec
49
- requirement: &70208657499660 !ruby/object:Gem::Requirement
49
+ requirement: &70232941135400 !ruby/object:Gem::Requirement
50
50
  none: false
51
51
  requirements:
52
52
  - - ~>
@@ -54,10 +54,10 @@ dependencies:
54
54
  version: 1.0.3
55
55
  type: :development
56
56
  prerelease: false
57
- version_requirements: *70208657499660
57
+ version_requirements: *70232941135400
58
58
  - !ruby/object:Gem::Dependency
59
59
  name: mountain_berry_fields-magic_comments
60
- requirement: &70208657499120 !ruby/object:Gem::Requirement
60
+ requirement: &70232941134860 !ruby/object:Gem::Requirement
61
61
  none: false
62
62
  requirements:
63
63
  - - ~>
@@ -65,7 +65,7 @@ dependencies:
65
65
  version: 1.0.1
66
66
  type: :development
67
67
  prerelease: false
68
- version_requirements: *70208657499120
68
+ version_requirements: *70232941134860
69
69
  description: Framework to aid in handrolling mock/spy objects.
70
70
  email:
71
71
  - josh.cheek@gmail.com