object_momma 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in object_momma.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 ntl
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,197 @@
1
+ # ObjectMomma
2
+
3
+ An Object Mother implementation in ruby
4
+
5
+ ## Description
6
+
7
+ ObjectMomma is a gem that implements the Object Mother pattern for managing test data. It shares a lot of goals with FactoryGirl, but represents a fundamentally different approach. See [this article by Martin Fowler](http://martinfowler.com/bliki/ObjectMother.html) for more information.
8
+
9
+ ## Vs. Fixtures and Factories
10
+
11
+ The Object Mother pattern shares some benefits with fixtures -- namely, by assigning fixed names (I call them *child identifiers*) to the objects you define, you can mentally associate the properties of each object with those names. For instance, the user called "Joe Spammer" may be used pervasively throughout tests that cover comment moderation. You can even build larger scenarios that can be re-used over and over by your team.
12
+
13
+ However, unlike fixtures, instead of managing yml files that directly manipulate database state, you can build out your objects in ruby with all of your model behaviors available. In this way, you get some of the best benefits of both fixtures and traditional factories.
14
+
15
+ The biggest benefit of Object Mother over both fixtures and factories is that they are not tied to ActiveRecord, so you can actually use ObjectMomma to refactor your entire model structure if you need to. ObjectMomma's goal is to facilitate data set up for your acceptance tests, not unit tests, so there is no reason that you couldn't use ObjectMomma alongside FactoryGirl or fixtures. This approach is similar to using cucumber for acceptance tests and rspec or Test::Unit for your unit/controller tests.
16
+
17
+ ## Installation
18
+
19
+ Install the gem locally:
20
+
21
+ gem install object_momma
22
+
23
+ Or add it to your Gemfile:
24
+
25
+ gem 'object_momma'
26
+
27
+ and run `bundle install` from your shell.
28
+
29
+ ## Usage
30
+
31
+ ### How your spec should look
32
+
33
+ `rspec/capybara`:
34
+
35
+ feature "Moderating comments" do
36
+ let(:post) do
37
+ ObjectMomma.post("Birds")
38
+ end
39
+
40
+ background do
41
+ ObjectMomma.spawn_comment("Joe Schmoe's Comment on the Post about Birds")
42
+ end
43
+
44
+ scenario "Up voting a comment" do
45
+ visit post_path(post)
46
+ # etc.
47
+ end
48
+ end
49
+
50
+ `Test::Unit`:
51
+
52
+ class CommentModeration < ActionController::IntegrationTest
53
+ def setup
54
+ @post = ObjectMomma.post("Birds")
55
+ ObjectMomma.spawn_post("Joe Schmoe's Comment on the Post About Birds")
56
+ end
57
+
58
+ def test_upvoting_a_comment
59
+ visit post_path(post)
60
+ # etc.
61
+ end
62
+ end
63
+
64
+ ### Using ObjectMomma
65
+
66
+ To create new objects, use methods that start with `ObjectMomma.spawn_*`, e.g. `ObjectMomma.spawn_user`:
67
+
68
+ => user = ObjectMomma.spawn_user("Joe Schmoe")
69
+ -> <#User:0xdeadbeef>
70
+
71
+ In this case above, "Joe Schmoe" can be thought of as the *child identifier* for the particular child we're creating.
72
+
73
+ Some objects are composed of other objects, and they can actually nest those child identifiers neatly, e.g. `Joe Schmoe's Comment on the Post about Birds.` You can spawn them by either referring to their composite child identifier, or by a hash mapping the associated object names to their identifiers:
74
+
75
+ => comment = ObjectMomma.spawn_comment("Joe Schmoe's Comment on the Post about Birds")
76
+ => comment = ObjectMomma.spawn_comment(user: "Joe Schmoe", post: "Birds")
77
+
78
+ `ObjectMomma.spawn_*` will raise an `ObjectMomma::ObjectExists` exception if the object has already been spawned. To simply return the object if it does exist, omit the `spawn_` and invoke ObjectMomma thusly:
79
+
80
+ => ObjectMomma.spawn("Joe Schmoe") # Works the first time
81
+ => ObjectMomma.spawn("Joe Schmoe") # Now raises an ObjectExists exception
82
+ => ObjectMomma.user("Joe Schmoe") # Works fine :)
83
+
84
+ If you want to raise an error if the object *doesn't* exist, then `ObjectMomma.find_*` will raise an `ObjectMomma::ObjectNotFound` exception in that case:
85
+
86
+ => ObjectMomma.find("Joe Schmoe") # Raises ObjectNotFound exception
87
+ => ObjectMomma.spawn("Joe Schmoe") # Works fine :)
88
+ => ObjectMomma.find("Joe Schmoe") # Does not raise exception
89
+
90
+ You can also build a bunch of objects all at once with `ObjectMomma.spawn`:
91
+
92
+ => ObjectMomma.spawn({
93
+ posts: ["Birds", "Middle Earth", "Sports"],
94
+ users: ["Joe Schmoe", "Scott Pilgrim"],
95
+ comment: "Billy Pilgrim's Comment on Post about Cooking"
96
+ })
97
+
98
+ The idea with the single call to `ObjectMomma.spawn` is to be able to encapsulate complicated data setup with a simple, semantic call.
99
+
100
+ ### Teaching ObjectMomma how to build new types of objects
101
+
102
+ The examples above assumed that ObjectMomma knew how to build out the objects in question. To actually teach ObjectMomma how to build the objects, consider:
103
+
104
+ In `spec/object_momma/user.rb`:
105
+
106
+ class ObjectMomma::UserBuilder < ObjectMomma::Builder
107
+ def first_or_initialize
108
+ User.where(full_name: self.child_id)
109
+ end
110
+
111
+ def build!(user)
112
+ user.full_name = child.child_id
113
+ end
114
+ end
115
+
116
+ The method `#first_or_initialize` has the most important role of the builder: it grabs the object from the persistence layer if it exists, otherwise, it initializes a new object.
117
+
118
+ After grabbing an object from `#first_or_initialize`, ObjectMomma will invoke `#build!` if and only if the object hasn't yet been persisted. `#build!` will actually set up the data and persist the object itself.
119
+
120
+ #### Composed Builders
121
+
122
+ You can also implement builders that know how to compose objects from their associated objects:
123
+
124
+ class ObjectMomma::CommentBuilder < ObjectMomma::Builder
125
+ child_id { "#{author}'s Comment on #{post}" }
126
+ has_siblings :post, author: user
127
+
128
+ def first_or_initialize
129
+ Comment.where(user_id: self.author.id, post_id: self.post.id)
130
+ end
131
+
132
+ def build!(object)
133
+ if object.user.spammer?
134
+ object.text = "Check out teh cheap pillz from mycheappillz.com!!!"
135
+ elsif object.post.subject == :birds
136
+ # etc. …
137
+ end
138
+ end
139
+ end
140
+
141
+ It is important to note the distinction between `self` and `object` in these cases. `self` refers to the builder which has been hydrated with properties from the `child_id` block, whereas `object` refers to the child object itself.
142
+
143
+ ### But I don't *want* to use ActiveRecord!
144
+
145
+ The only requirement ObjectMomma imposes on the objects that are spawned via your `first_or_initialize` methods is that they respond to `#persisted?` the way ActiveRecord objects do. The gem itself does not depend on ActiveRecord at all; feel free to use whatever persistence layer you want. Consider overriding `ObjectMomma::Builder.is_persisted?` if your objects don't respond to `#persisted?`.
146
+
147
+ Your builder's `first_or_initialize` method can return a Scope, as well. ObjectMomma will simply call `first_or_initialize` for you.
148
+
149
+ ### Serialized Attributes
150
+
151
+ Supplying all the fictional data for your different children can be tedious and ugly. You may want to store canned attributes for your objects in YAML files, similar to fixtures. This features is completely optional. The attributes will get passed to your builders' #build! method as the second argument. An empty hash will be supplied if ObjectMomma couldn't find and parse the attributes for you. Example:
152
+
153
+ # spec/object_momma/user_builder.rb
154
+ class ObjectMomma::UserBuilder
155
+
156
+
157
+ def build!(user, attributes)
158
+ user.attributes = attributes
159
+ end
160
+
161
+
162
+ end
163
+
164
+ The YAML file:
165
+
166
+ # spec/object_momma/attributes/users.yml
167
+ Scott Pilgrim:
168
+ username: "scott_pilgrim"
169
+ email: "spilgrim@zz.zzz"
170
+
171
+ To use:
172
+
173
+ => ObjectMother.spawn("Scott Pilgrim")
174
+ -> #<User:0xdeadbeef @username="scott_pilgrim", @email="spilgrim@zz.zzz", …>
175
+
176
+ ### Business in the front, party in the back
177
+
178
+ If you'd like to refer to ObjectMomma via the constant ObjectMother, then have it your way:
179
+
180
+ In `spec/spec_helper.rb`, add `ObjectMomma.mullet!` somewhere. Then:
181
+
182
+ => ObjectMother.spawn_user("Joe Dirt")
183
+
184
+ ## More Information
185
+
186
+ * [ObjectMomma by Martin Fowler](http://martinfowler.com/bliki/ObjectMother.html)
187
+
188
+ ### Credits
189
+
190
+ ObjectMomma was written by Nathan Ladd, with help from a few partners in crime:
191
+
192
+ * Josh Flanagan (jflanagan on github)
193
+ * Theo Mills
194
+
195
+ And, of course, I read about the ObjectMother pattern that I ruined^H^H^H^Himplemented from:
196
+
197
+ * Martin Fowler
data/Rakefile ADDED
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+
4
+ task :console do
5
+ require "pp"
6
+ require "irb"
7
+
8
+ require "object_momma"
9
+ ObjectMomma.mullet!
10
+
11
+ require File.join(File.dirname(__FILE__), "spec/fixtures/blog_post_voting_classes")
12
+
13
+ ARGV.clear
14
+ IRB.start
15
+ end
16
+
17
+ require "rspec/core/rake_task"
18
+ RSpec::Core::RakeTask.new('spec')
19
+
20
+ task :default => :spec
@@ -0,0 +1,164 @@
1
+ module ObjectMomma
2
+ VALID_IDENTIFIER_CHARS = %q{-\w\s_'\"\.}
3
+
4
+ class Builder
5
+ extend ObjectMomma::ClassAttributes
6
+
7
+ class_attribute :child_id_serializer, :siblings
8
+
9
+ attr_reader :child
10
+ private :child
11
+
12
+ def build_child_from_hash(hash, &block)
13
+ if has_child_id_serializer?
14
+ child.child_id = self.class.run_with_binding(hash)
15
+
16
+ hash.each do |name, value|
17
+ if has_siblings?
18
+ sibling_object_type = self.class.siblings[name]
19
+ if sibling_object_type
20
+ if value.is_a?(ObjectMomma::Child)
21
+ child = value
22
+ else
23
+ child = yield(sibling_object_type, value)
24
+ end
25
+ value = child.child_object
26
+ end
27
+ end
28
+
29
+ ivar_name = "@#{name}"
30
+ instance_variable_set(ivar_name, value)
31
+ self.singleton_class.instance_exec(name, ivar_name) do |name, ivar_name|
32
+ define_method(name) { instance_variable_get(ivar_name) }
33
+ end
34
+ end
35
+ else
36
+ child.child_id = hash.delete(:child_id)
37
+ end
38
+ end
39
+
40
+ def build!(*args)
41
+ raise Objectmomma::SubclassNotImplemented
42
+ end
43
+
44
+ def child_id
45
+ child.child_id
46
+ end
47
+
48
+ def initialize(child)
49
+ @child = child
50
+ end
51
+
52
+ def is_persisted?(object)
53
+ if object.respond_to?(:persisted?)
54
+ object.persisted?
55
+ else
56
+ raise ObjectMomma::SubclassNotImplemented, "Override #is_persisted? "\
57
+ "to support objects that do not respond to #persisted?"
58
+ end
59
+ end
60
+
61
+ def self.builder_for(object_type)
62
+ if ObjectMomma.builder_path
63
+ builder_file = File.join(ObjectMomma.builder_path, "#{object_type}_builder.rb")
64
+ require builder_file if File.size?(builder_file)
65
+ end
66
+
67
+ classified_name = "_#{object_type}Builder".gsub(/_\w/) do |underscored|
68
+ underscored[1].upcase
69
+ end
70
+
71
+ ObjectMomma.const_get(classified_name)
72
+ end
73
+
74
+ def self.has_child_id_serializer?
75
+ self.child_id_serializer.respond_to?(:to_proc)
76
+ end
77
+
78
+ def self.has_siblings?
79
+ self.siblings.is_a?(Hash)
80
+ end
81
+
82
+ def self.run_with_binding(props = {}, &block)
83
+ Object.new.tap do |o|
84
+ props.each do |name, value|
85
+ ivar_name = "@#{name}".to_sym
86
+ o.singleton_class.class_eval do
87
+ define_method(name) { instance_variable_get(ivar_name) }
88
+ end
89
+ o.instance_variable_set(ivar_name, value)
90
+ end
91
+
92
+ o.singleton_class.class_eval(&block) if block_given?
93
+ end.instance_exec(&child_id_serializer)
94
+ end
95
+
96
+ def self.string_to_hash(object_type, string)
97
+ builder = builder_for(object_type)
98
+
99
+ if builder.has_child_id_serializer?
100
+ vars = []
101
+
102
+ builder.run_with_binding(vars: vars) do
103
+ def method_missing(sym, *args, &block)
104
+ return super unless args.empty? && !block_given?
105
+ vars << sym unless vars.include?(sym)
106
+ end
107
+ end
108
+
109
+ string_matcher = "([#{VALID_IDENTIFIER_CHARS}]+)"
110
+ regex_string = builder.run_with_binding(string_matcher: string_matcher) do
111
+ def method_missing(sym, *args, &block)
112
+ return super unless args.empty? && !block_given?
113
+ string_matcher
114
+ end
115
+ end
116
+
117
+ regex = Regexp.new("^#{regex_string}$")
118
+ matches = string.match(regex).to_a[1..-1]
119
+
120
+ if matches.nil?
121
+ raise BadChildIdentifier, "Bad child_id `#{string}' for builder "\
122
+ "`#{name}'"
123
+ end
124
+
125
+ Hash[vars.zip(matches)]
126
+ else
127
+ {child_id: string}
128
+ end
129
+ end
130
+
131
+ class << self
132
+ def child_id(&block)
133
+ self.child_id_serializer = block
134
+ end
135
+
136
+ def has_siblings(*args)
137
+ if args.last.is_a?(Hash)
138
+ hash = args.pop.each_with_object({}) do |(sibling_name, object_type), hash|
139
+ hash[sibling_name] = object_type
140
+ end
141
+ else
142
+ hash = {}
143
+ end
144
+
145
+ args.each_with_object(hash) do |sibling_name|
146
+ object_type = sibling_name
147
+ hash[sibling_name] = object_type
148
+ end
149
+
150
+ self.siblings = hash
151
+ end
152
+ end
153
+
154
+ private
155
+
156
+ def has_child_id_serializer?
157
+ self.class.has_child_id_serializer?
158
+ end
159
+
160
+ def has_siblings?
161
+ self.class.has_siblings?
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,126 @@
1
+ module ObjectMomma
2
+ ACTUALIZE_STRATEGIES = [:create, :find, :find_or_create]
3
+
4
+ class Child
5
+ attr_accessor :child_id
6
+ attr_reader :actualize_strategy, :builder, :object_type
7
+ alias_method :to_s, :child_id
8
+
9
+ def initialize(object_type, hash, actualize_strategy)
10
+ unless ACTUALIZE_STRATEGIES.include?(actualize_strategy)
11
+ raise ArgumentError, "Invalid actualize strategy "\
12
+ "`#{actualize_strategy}'; valid values are "\
13
+ "#{ACTUALIZE_STRATEGIES.map(&:to_s).join(', ')}"
14
+ end
15
+
16
+ @actualize_strategy = actualize_strategy
17
+ @builder = ObjectMomma.builder_for(object_type).new(self)
18
+ @object_type = object_type
19
+
20
+ builder.build_child_from_hash(hash) do |sibling_object_type, sibling_id|
21
+ self.class.new(sibling_object_type, sibling_id, @actualize_strategy)
22
+ end
23
+ end
24
+
25
+ def child_object
26
+ @child_object ||= actualize_child_object
27
+ end
28
+
29
+ class << self
30
+ alias_method :original_new, :new
31
+
32
+ def new(object_type, string_or_hash, *args)
33
+ if string_or_hash.is_a?(String)
34
+ hash = Builder.string_to_hash(object_type, string_or_hash)
35
+ elsif string_or_hash.is_a?(Hash)
36
+ hash = string_or_hash
37
+ else
38
+ raise ArgumentError, "Must instantiate a Child with a String or a "\
39
+ "Hash, not a #{string_or_hash.class.name}"
40
+ end
41
+
42
+ original_new(object_type, hash, *args)
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ def actualize_child_object
49
+ object = builder.first_or_initialize
50
+
51
+ if object.respond_to?(:first_or_initialize)
52
+ object = object.first_or_initialize
53
+ end
54
+
55
+ if builder.is_persisted?(object)
56
+ if actualize_strategy == :create
57
+ raise ObjectMomma::ObjectExists, "Child `#{child_id}' created by "\
58
+ "`#{builder.class.name}' exists already"
59
+ end
60
+ else
61
+ if actualize_strategy == :find
62
+ raise ObjectMomma::ObjectNotFound, "Child `#{child_id}' created by "\
63
+ "`#{builder.class.name}' does not yet exist"
64
+ end
65
+
66
+ # arity of -2: def build(object, attrs = {}) (optional)
67
+ # arity of 2: def build(object, attrs) (required)
68
+ if [-2, 2].include?(builder.method(:build!).arity)
69
+ builder.build!(object, attributes_for_child)
70
+ else
71
+ builder.build!(object)
72
+ end
73
+
74
+ unless builder.is_persisted?(object)
75
+ raise ObjectMomma::NotPersisted, "Child `#{child_id}' was created "\
76
+ "by `#{builder.class.name}' but wasn't persisted"
77
+ end
78
+ end
79
+
80
+ if builder.respond_to?(:decorate!)
81
+ builder.decorate!(object)
82
+ end
83
+
84
+ object
85
+ end
86
+
87
+ def attributes_for_child
88
+ return {} unless ObjectMomma.use_serialized_attributes
89
+
90
+ # Pluralize
91
+ if object_type.to_s.chars.to_a.last == "s"
92
+ file_name = object_type
93
+ else
94
+ file_name = "#{object_type}s"
95
+ end
96
+
97
+ path = File.join(ObjectMomma.serialized_attributes_path, "#{file_name}.yml")
98
+
99
+ if File.size?(path)
100
+ File.open(path) do |yml_file|
101
+ attributes_by_child_id = YAML::load(yml_file)
102
+ return recursively_symbolize_hash(attributes_by_child_id.fetch(child_id, {}))
103
+ end
104
+ end
105
+
106
+ {}
107
+ end
108
+
109
+ def recursively_symbolize_hash(hash = {})
110
+ recurse = lambda { |in_hash|
111
+ {}.tap do |out_hash|
112
+ in_hash.each do |key, in_value|
113
+ out_value = in_value.is_a?(Hash) ? recurse.call(in_value) : in_value.dup
114
+ if key.respond_to?(:to_sym)
115
+ out_hash[key.to_sym] = out_value
116
+ else
117
+ out_hash[key] = out_value
118
+ end
119
+ end
120
+ end
121
+ }
122
+
123
+ recurse.call(hash)
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,19 @@
1
+ module ObjectMomma
2
+ module ClassAttributes
3
+ # See http://www.ruby-forum.com/topic/197051
4
+ def class_attribute(*attributes)
5
+ singleton_class.class_eval do
6
+ attr_accessor *attributes
7
+ end
8
+
9
+ @class_attributes ||= []
10
+ @class_attributes.concat(attributes)
11
+ end
12
+
13
+ def inherited(subclass)
14
+ @class_attributes.each do |attribute|
15
+ subclass.send("#{attribute}=", self.send(attribute))
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,28 @@
1
+ module ObjectMomma
2
+ module Config
3
+ def self.extended(base)
4
+ base.singleton_class.instance_eval do
5
+ attr_reader :builder_path
6
+ attr_reader :serialized_attributes_path, :use_serialized_attributes
7
+ end
8
+ end
9
+
10
+ def builder_path=(path)
11
+ unless path.nil? || File.directory?(path)
12
+ raise ArgumentError, "`#{path}' is not a valid directory"
13
+ end
14
+ @builder_path = path
15
+ end
16
+
17
+ def serialized_attributes_path=(path)
18
+ unless File.directory?(path)
19
+ raise ArgumentError, "`#{path}' is not a valid directory"
20
+ end
21
+ @serialized_attributes_path = path
22
+ end
23
+
24
+ def use_serialized_attributes=(true_or_false)
25
+ @use_serialized_attributes = true_or_false ? true : false
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,80 @@
1
+ module ObjectMomma
2
+ module ModuleMethods
3
+ def builder_for(object_type)
4
+ ObjectMomma::Builder.builder_for(object_type)
5
+ end
6
+
7
+ def method_missing(method_name, *args, &block)
8
+ return super unless respond_to?(method_name)
9
+ return super if block_given?
10
+
11
+ object_type, actualize_strategy = object_type_and_actualize_strategy_from_method_name(method_name)
12
+ args.push(actualize_strategy)
13
+
14
+ child = ObjectMomma::Child.new(object_type, *args)
15
+ child.child_object
16
+ end
17
+
18
+ def mullet!
19
+ return false if Object.const_defined?(:ObjectMother)
20
+
21
+ object_mother = Class.new(BasicObject) do
22
+ def self.method_missing(*args)
23
+ ObjectMomma.send(*args)
24
+ end
25
+ end
26
+
27
+ Object.const_set(:ObjectMother, object_mother)
28
+ true
29
+ end
30
+
31
+ def object_type_and_actualize_strategy_from_method_name(method_name)
32
+ # Try ObjectMomma.user
33
+ begin
34
+ builder_for(method_name)
35
+ object_type = method_name.to_sym
36
+ return [object_type, :find_or_create]
37
+ rescue NameError
38
+ end
39
+
40
+ # Try ObjectMomma.spawn_user, ObjectMomma.find_user
41
+ public_method_name, object_type = [*method_name.to_s.match(/^(create|find|spawn)_(\w+)$/).to_a[1..-1]].compact.map(&:to_sym)
42
+ return nil if object_type.nil?
43
+
44
+ begin
45
+ builder_for(object_type)
46
+ if public_method_name == :spawn
47
+ actualize_strategy = :find_or_create
48
+ else
49
+ actualize_strategy = public_method_name
50
+ end
51
+ [object_type, actualize_strategy]
52
+ rescue NameError
53
+ nil
54
+ end
55
+ end
56
+ alias_method :parse_method_name, :object_type_and_actualize_strategy_from_method_name
57
+
58
+ def respond_to?(method_name, *args)
59
+ return true if super
60
+ parse_method_name(method_name).nil? ? false : true
61
+ end
62
+
63
+ def spawn(hash = {})
64
+ hash.each do |object_type, child_id_or_ids|
65
+ begin
66
+ builder_for(object_type)
67
+ rescue NameError => ne
68
+ singularized = object_type.to_s.chomp('s').to_sym
69
+ raise ne if singularized == object_type
70
+
71
+ builder_for(singularized)
72
+ object_type = singularized
73
+ end
74
+
75
+ child_ids = [*child_id_or_ids]
76
+ child_ids.each { |child_id| send("spawn_#{object_type}", child_id) }
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,3 @@
1
+ module ObjectMomma
2
+ VERSION = "0.9.0"
3
+ end
@@ -0,0 +1,21 @@
1
+ require 'yaml'
2
+
3
+ require 'object_momma/class_attributes'
4
+
5
+ require 'object_momma/builder'
6
+ require 'object_momma/config'
7
+ require 'object_momma/child'
8
+ require 'object_momma/module_methods'
9
+ require 'object_momma/version'
10
+
11
+ module ObjectMomma
12
+ BadChildIdentifier = Class.new(StandardError)
13
+ BadSerializer = Class.new(StandardError)
14
+ NotPersisted = Class.new(StandardError)
15
+ ObjectExists = Class.new(StandardError)
16
+ ObjectNotFound = Class.new(StandardError)
17
+ SubclassNotImplemented = Class.new(StandardError)
18
+
19
+ self.extend(ObjectMomma::ModuleMethods)
20
+ self.extend(ObjectMomma::Config)
21
+ end
@@ -0,0 +1,19 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/object_momma/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["ntl"]
6
+ gem.email = ["nathanladd@gmail.com"]
7
+ gem.description = %q{object_momma is an Object Mother implementation in ruby}
8
+ gem.summary = %q{object_momma is an Object Mother implementation in ruby}
9
+ gem.homepage = ""
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "object_momma"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = ObjectMomma::VERSION
17
+
18
+ gem.add_development_dependency 'rspec'
19
+ end
@@ -0,0 +1,135 @@
1
+ File.dirname(__FILE__).tap do |current_directory|
2
+ %w{fake_model post user comment vote}.each do |file|
3
+ require File.join(current_directory, file)
4
+ end
5
+ end
6
+
7
+ module ObjectMomma
8
+ class VoteBuilder < Builder
9
+ has_siblings :comment, :voter => :user
10
+ child_id { "#{voter}'s #{vote_type} for #{comment}" }
11
+
12
+ def first_or_initialize
13
+ Vote.where({
14
+ comment_id: comment.id,
15
+ user_id: voter.id,
16
+ type: vote_type.downcase
17
+ })
18
+ end
19
+
20
+ def decorate(vote)
21
+ # The regular Vote class doesn't implement this method, but Vote objects
22
+ # instantiated via ObjectMomma will get this behavior. Be careful with
23
+ # this feature, it can make a mess!
24
+ def vote.switch_vote!
25
+ if upvote?
26
+ self.type = 'downvote'
27
+ else
28
+ self.type = 'upvote'
29
+ end
30
+ end
31
+ end
32
+
33
+ def build!(vote)
34
+ # This is demonstration only. In a real ActiveRecord scenario, that
35
+ # scope returned by #first_or_initialize would have set this stuff for
36
+ # us. It may be possible to automatically do this in a generic way.
37
+ vote.comment = self.comment
38
+ vote.type = self.vote_type
39
+ vote.user = self.voter
40
+
41
+ # This is basic "set my data according to who I am" logic:
42
+ if vote.user.politician?
43
+ vote.switch_vote!
44
+ end
45
+
46
+ vote.save!
47
+ end
48
+ end
49
+
50
+ class UserBuilder < Builder
51
+ def first_or_initialize
52
+ User.where(email: test_email)
53
+ end
54
+
55
+ def decorate!(user)
56
+ def user.politician?
57
+ full_name == "John Adams"
58
+ end
59
+ end
60
+
61
+ def build!(user, attributes = {})
62
+ # ActiveRecord's scope returned by #first_or_initialize would do this for
63
+ # us under normal circumstances.
64
+ user.email = attributes[:email] || test_email
65
+ user.username = attributes[:username] || test_username
66
+ user.full_name = child_id
67
+
68
+ user.save!
69
+ end
70
+
71
+ private
72
+
73
+ # A child id of 'Scott Pilgrim' becomes 'scottpilgrim@zz.zzz'
74
+ def test_email
75
+ "#{test_username}@zz.zzz"
76
+ end
77
+
78
+ # A child id of 'Scott Pilgrim' becomes 'scottpilgrim'
79
+ def test_username
80
+ self.child_id.split(' ', 2).map(&:downcase).join
81
+ end
82
+ end
83
+
84
+ class PostBuilder < Builder
85
+ child_id { "Post about #{subject}" }
86
+
87
+ def first_or_initialize
88
+ Post.where(title: title_from_subject)
89
+ end
90
+
91
+ def build!(post)
92
+ post.title = title_from_subject
93
+ post.subject = self.subject
94
+ post.body = body_from_subject
95
+
96
+ post.save!
97
+ end
98
+
99
+ private
100
+
101
+ def body_from_subject
102
+ case self.subject
103
+ when "Comic Books" then "Batman is the best comic book of all time"
104
+ when "Politics" then "John Adams was a great leader."
105
+ else "Lorem Ipsum"
106
+ end
107
+ end
108
+
109
+ def title_from_subject
110
+ case self.subject
111
+ when "Comic Books" then "Batman"
112
+ else "My Thoughts on #{self.subject}"
113
+ end
114
+ end
115
+ end
116
+
117
+ class CommentBuilder < Builder
118
+ has_siblings :post, :author => :user
119
+ child_id { "#{author}'s Comment on #{post}" }
120
+
121
+ def first_or_initialize
122
+ Comment.where({
123
+ post_id: post.id,
124
+ user_id: author.id
125
+ })
126
+ end
127
+
128
+ def build!(comment)
129
+ comment.post = self.post
130
+ comment.user = self.author
131
+
132
+ comment.save!
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,8 @@
1
+ class Comment
2
+ include FakeModel
3
+
4
+ belongs_to :post
5
+ belongs_to :user
6
+
7
+ fake_column :text
8
+ end
@@ -0,0 +1,90 @@
1
+ module FakeModel
2
+ def self.included(base)
3
+ base.extend(ClassMethods)
4
+ end
5
+
6
+ def ==(other_object)
7
+ self.object_id == other_object.object_id
8
+ end
9
+
10
+ def initialize(attributes = {})
11
+ attributes.each do |key, value|
12
+ instance_variable_set("@#{key}".to_sym, value)
13
+ end
14
+
15
+ self.class.instances << self
16
+ end
17
+
18
+ def id
19
+ return nil unless persisted?
20
+
21
+ persisted_instances = self.class.instances.select(&:persisted?)
22
+ index = persisted_instances.index { |instance| instance == self }
23
+ index + 1
24
+ end
25
+
26
+ def persisted?
27
+ @is_persisted ? true : false
28
+ end
29
+
30
+ def save
31
+ @is_persisted = true
32
+ true
33
+ end
34
+ alias_method :save!, :save
35
+
36
+ module ClassMethods
37
+ def belongs_to(association_name)
38
+ define_method(association_name) do
39
+ instance_variable_get("@#{association_name}")
40
+ end
41
+
42
+ define_method("#{association_name}_id") do
43
+ send(association_name).id
44
+ end
45
+
46
+ define_method("#{association_name}=") do |value|
47
+ instance_variable_set("@#{association_name}", value)
48
+ end
49
+ end
50
+
51
+ def destroy_all
52
+ instances.clear
53
+ end
54
+
55
+ def instances
56
+ @instances ||= []
57
+ end
58
+
59
+ def fake_column(column_name)
60
+ class_eval { attr_accessor(column_name) }
61
+ end
62
+
63
+ def fake_columns(*column_names)
64
+ column_names.each { |column_name| fake_column(column_name) }
65
+ end
66
+
67
+ def where(conditions = {})
68
+ object = instances.detect do |object|
69
+ conditions.all? { |attr, value| object.send(attr) == value }
70
+ end
71
+
72
+ object ||= self.new
73
+
74
+ scope = BasicObject.new
75
+
76
+ scope.instance_exec(object) { |o| @__scope_object__ = o }
77
+
78
+ class << scope
79
+ def first_or_initialize
80
+ @__scope_object__
81
+ end
82
+ def method_missing(method_name, *args, &block)
83
+ @__scope_object__.send(method_name, *args, &block)
84
+ end
85
+ end
86
+
87
+ scope
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,5 @@
1
+ class Post
2
+ include FakeModel
3
+
4
+ fake_columns :body, :subject, :title
5
+ end
@@ -0,0 +1,18 @@
1
+ class ObjectMomma::ThingBuilder < ObjectMomma::Builder
2
+ class Thing
3
+ attr_accessor :a_property, :another_property
4
+
5
+ def persisted?
6
+ a_property.nil? ? false : true
7
+ end
8
+ end
9
+
10
+ def first_or_initialize
11
+ Thing.new
12
+ end
13
+
14
+ def build!(thing)
15
+ thing.a_property = :foo
16
+ thing.another_property = :bar
17
+ end
18
+ end
@@ -0,0 +1,5 @@
1
+ class User
2
+ include FakeModel
3
+
4
+ fake_columns :email, :full_name, :username
5
+ end
@@ -0,0 +1,3 @@
1
+ Scott Pilgrim:
2
+ email: foobar@zz.zzz
3
+ username: lovemuscle23
@@ -0,0 +1,16 @@
1
+ class Vote
2
+ include FakeModel
3
+
4
+ belongs_to :comment
5
+ belongs_to :user
6
+
7
+ fake_column :type
8
+
9
+ def downvote?
10
+ type == 'downvote'
11
+ end
12
+
13
+ def upvote?
14
+ type == 'upvote'
15
+ end
16
+ end
@@ -0,0 +1,223 @@
1
+ require 'object_momma'
2
+ require File.join(File.dirname(__FILE__), 'fixtures/blog_post_voting_classes')
3
+
4
+ describe ObjectMomma do
5
+ before do
6
+ User.destroy_all
7
+ Post.destroy_all
8
+ Comment.destroy_all
9
+ Vote.destroy_all
10
+ end
11
+
12
+ let(:vote_child_id) do
13
+ "Billy Pilgrim's Upvote for Scott Pilgrim's Comment on Post about Comic Books"
14
+ end
15
+
16
+ context "A simple object type (User)" do
17
+ shared_examples_for "Scott Pilgrim" do
18
+ it("is a User") { user.should be_a(User) }
19
+ it("has its' email set properly") { user.email.should == "scottpilgrim@zz.zzz" }
20
+ it("has its' username set properly") { user.username.should == "scottpilgrim" }
21
+ it("is persisted") { user ; ObjectMomma.find_user("Scott Pilgrim") }
22
+ end
23
+
24
+ context "when instantiated with a Hash" do
25
+ let(:user) { ObjectMomma.spawn_user(child_id: "Scott Pilgrim") }
26
+ it_behaves_like "Scott Pilgrim"
27
+ end
28
+
29
+ context "when instantiated with a String" do
30
+ let(:user) { ObjectMomma.spawn_user("Scott Pilgrim") }
31
+ it_behaves_like "Scott Pilgrim"
32
+ end
33
+ end
34
+
35
+ context "An object type that has a child id constructor (Post)" do
36
+ shared_examples_for "Post about Comic Books" do
37
+ it("is a Post") { post.should be_a(Post) }
38
+ it("has its' title set properly") { post.title.should == "Batman" }
39
+ it("has its' subject set properly") { post.subject.should == "Comic Books" }
40
+ it("is persisted") { post ; ObjectMomma.find_post("Post about Comic Books") }
41
+ end
42
+
43
+ context "when instantiated with a Hash" do
44
+ let(:post) { ObjectMomma.spawn_post(subject: "Comic Books") }
45
+ it_behaves_like "Post about Comic Books"
46
+ end
47
+
48
+ context "when instantiated with a String" do
49
+ let(:post) { ObjectMomma.spawn_post("Post about Comic Books") }
50
+ it_behaves_like "Post about Comic Books"
51
+ end
52
+ end
53
+
54
+ context "An object with simple siblings (Comment)" do
55
+ shared_examples_for "Scott Pilgrim's Comment on Post about Comic Books" do
56
+ it("is a Comment") { comment.should be_a(Comment) }
57
+ it("has its' user set properly") do
58
+ comment.user.object_id.should == ObjectMomma.find_user("Scott Pilgrim").object_id
59
+ end
60
+ it("has its' post set properly") do
61
+ comment.post.object_id.should == ObjectMomma.find_post("Post about Comic Books").object_id
62
+ end
63
+ it("is persisted") do
64
+ comment
65
+ ObjectMomma.find_comment("Scott Pilgrim's Comment on Post about Comic Books")
66
+ end
67
+ end
68
+
69
+ context "when instantiated with a Hash" do
70
+ let(:comment) do
71
+ ObjectMomma.spawn_comment({
72
+ author: "Scott Pilgrim",
73
+ post: "Post about Comic Books"
74
+ })
75
+ end
76
+ it_behaves_like "Scott Pilgrim's Comment on Post about Comic Books"
77
+ end
78
+
79
+ context "when instantiated with a String" do
80
+ let(:comment) do
81
+ ObjectMomma.spawn_comment("Scott Pilgrim's Comment on Post about Comic Books")
82
+ end
83
+ it_behaves_like "Scott Pilgrim's Comment on Post about Comic Books"
84
+ end
85
+ end
86
+
87
+ context "An object with nested siblings (Vote)" do
88
+ context "when instantiated with a String" do
89
+ let(:vote) do
90
+ ObjectMomma.spawn_vote(vote_child_id)
91
+ end
92
+ it("is a Vote") { vote.should be_a(Vote) }
93
+ it("has its' user set properly") do
94
+ vote.user.object_id.should == ObjectMomma.find_user("Billy Pilgrim").object_id
95
+ end
96
+ it("has its' comment set properly") do
97
+ vote.comment.object_id.should == ObjectMomma.find_comment("Scott Pilgrim's Comment on Post about Comic Books").object_id
98
+ end
99
+ end
100
+ end
101
+
102
+ context "An object with serialized attributes" do
103
+ before do
104
+ ObjectMomma.use_serialized_attributes = true
105
+ ObjectMomma.serialized_attributes_path = File.expand_path("../fixtures", __FILE__)
106
+ end
107
+
108
+ after do
109
+ ObjectMomma.use_serialized_attributes = false
110
+ end
111
+
112
+ let(:user) { ObjectMomma.spawn_user("Scott Pilgrim") }
113
+
114
+ it "pulls in the attributes and loads them into the object" do
115
+ user.email.should == "foobar@zz.zzz"
116
+ user.username.should == "lovemuscle23"
117
+ end
118
+ end
119
+
120
+ context "An object whose builder is stored in the builder path" do
121
+ after do
122
+ ObjectMomma.builder_path = nil
123
+ end
124
+
125
+ it "Loads the builder ruby file from the path only after builder path is set" do
126
+ lambda {
127
+ ObjectMomma.spawn_thing("The Thing")
128
+ }.should raise_error(NoMethodError)
129
+
130
+ ObjectMomma.builder_path = File.expand_path("../fixtures", __FILE__)
131
+
132
+ thing = ObjectMomma.spawn_thing("The Thing")
133
+
134
+ thing.a_property.should == :foo
135
+ thing.another_property.should == :bar
136
+ end
137
+ end
138
+
139
+ context ".spawn" do
140
+ it "invokes #spawn_(object_type) for the supplied arguments" do
141
+ ObjectMomma.should_receive(:spawn_user).with("Billy Pilgrim").once
142
+ ObjectMomma.should_receive(:spawn_user).with("Scott Pilgrim").once
143
+ ObjectMomma.should_receive(:spawn_vote).with(vote_child_id).once
144
+ ObjectMomma.should_receive(:spawn_comment).with("Post about Politics").once
145
+
146
+ ObjectMomma.spawn({
147
+ users: ["Billy Pilgrim", "Scott Pilgrim"],
148
+ vote: vote_child_id,
149
+ comment: "Post about Politics"
150
+ })
151
+ end
152
+ end
153
+
154
+ context ".spawn_(object_type)" do
155
+ it "raises an ObjectMomma::BadChildIdentifier when it cannot parse child id" do
156
+ lambda {
157
+ ObjectMomma.spawn_post("Poast about Birds")
158
+ }.should raise_error(ObjectMomma::BadChildIdentifier)
159
+ end
160
+ end
161
+
162
+ context ".mullet!" do
163
+ before do
164
+ ObjectMomma.mullet!
165
+ end
166
+
167
+ after do
168
+ if Object.const_defined?(:ObjectMother)
169
+ Object.send(:remove_const, :ObjectMother)
170
+ end
171
+ end
172
+
173
+ it "defines ObjectMother" do
174
+ Object.const_defined?(:ObjectMother).should be_true
175
+ end
176
+
177
+ it "delegates everything to ObjectMomma" do
178
+ ObjectMomma.should_receive(:fizzle).with(:shizzle)
179
+ ObjectMother.fizzle(:shizzle)
180
+ end
181
+ end
182
+
183
+ context ".find_(object_type)" do
184
+ it "raises an ObjectMomma::ObjectNotFound exception when object does not exist" do
185
+ lambda {
186
+ ObjectMomma.find_user("Scott Pilgrim")
187
+ }.should raise_error(ObjectMomma::ObjectNotFound)
188
+ end
189
+
190
+ it "does not raise an ObjectMomma::ObjectNotFound exception when object does exist" do
191
+ ObjectMomma.spawn_user("Scott Pilgrim")
192
+ lambda {
193
+ ObjectMomma.find_user("Scott Pilgrim")
194
+ }.should_not raise_error(ObjectMomma::ObjectNotFound)
195
+ end
196
+ end
197
+
198
+ context ".create_(object_type)" do
199
+ it "does not raise an ObjectMomma::ObjectExists exception when object does not exist" do
200
+ lambda {
201
+ ObjectMomma.create_user("Scott Pilgrim")
202
+ }.should_not raise_error(ObjectMomma::ObjectExists)
203
+ end
204
+
205
+ it "raises an ObjectMomma::ObjectExists exception when object does exist" do
206
+ ObjectMomma.spawn_user("Scott Pilgrim")
207
+ lambda {
208
+ ObjectMomma.create_user("Scott Pilgrim")
209
+ }.should raise_error(ObjectMomma::ObjectExists)
210
+ end
211
+ end
212
+
213
+ context ".(object_type)" do
214
+ it "creates objects that don't exist, and finds objects that do" do
215
+ lambda {
216
+ ObjectMomma.user("Scott Pilgrim")
217
+ }.should_not raise_error(ObjectMomma::ObjectNotFound)
218
+ lambda {
219
+ ObjectMomma.user("Scott Pilgrim")
220
+ }.should_not raise_error(ObjectMomma::ObjectExists)
221
+ end
222
+ end
223
+ end
data/tmp/.gitkeep ADDED
File without changes
metadata ADDED
@@ -0,0 +1,104 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: object_momma
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 9
8
+ - 0
9
+ version: 0.9.0
10
+ platform: ruby
11
+ authors:
12
+ - ntl
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2012-11-28 00:00:00 -06:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: rspec
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ segments:
28
+ - 0
29
+ version: "0"
30
+ type: :development
31
+ version_requirements: *id001
32
+ description: object_momma is an Object Mother implementation in ruby
33
+ email:
34
+ - nathanladd@gmail.com
35
+ executables: []
36
+
37
+ extensions: []
38
+
39
+ extra_rdoc_files: []
40
+
41
+ files:
42
+ - .gitignore
43
+ - Gemfile
44
+ - LICENSE
45
+ - README.md
46
+ - Rakefile
47
+ - lib/object_momma.rb
48
+ - lib/object_momma/builder.rb
49
+ - lib/object_momma/child.rb
50
+ - lib/object_momma/class_attributes.rb
51
+ - lib/object_momma/config.rb
52
+ - lib/object_momma/module_methods.rb
53
+ - lib/object_momma/version.rb
54
+ - object_momma.gemspec
55
+ - spec/fixtures/blog_post_voting_classes.rb
56
+ - spec/fixtures/comment.rb
57
+ - spec/fixtures/fake_model.rb
58
+ - spec/fixtures/post.rb
59
+ - spec/fixtures/thing_builder.rb
60
+ - spec/fixtures/user.rb
61
+ - spec/fixtures/users.yml
62
+ - spec/fixtures/vote.rb
63
+ - spec/object_momma_spec.rb
64
+ - tmp/.gitkeep
65
+ has_rdoc: true
66
+ homepage: ""
67
+ licenses: []
68
+
69
+ post_install_message:
70
+ rdoc_options: []
71
+
72
+ require_paths:
73
+ - lib
74
+ required_ruby_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ segments:
79
+ - 0
80
+ version: "0"
81
+ required_rubygems_version: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ segments:
86
+ - 0
87
+ version: "0"
88
+ requirements: []
89
+
90
+ rubyforge_project:
91
+ rubygems_version: 1.3.6
92
+ signing_key:
93
+ specification_version: 3
94
+ summary: object_momma is an Object Mother implementation in ruby
95
+ test_files:
96
+ - spec/fixtures/blog_post_voting_classes.rb
97
+ - spec/fixtures/comment.rb
98
+ - spec/fixtures/fake_model.rb
99
+ - spec/fixtures/post.rb
100
+ - spec/fixtures/thing_builder.rb
101
+ - spec/fixtures/user.rb
102
+ - spec/fixtures/users.yml
103
+ - spec/fixtures/vote.rb
104
+ - spec/object_momma_spec.rb