factory_boy 1.0.5 → 2.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.
Files changed (42) hide show
  1. data/README.rdoc +68 -31
  2. data/lib/blank_slate.rb +3 -0
  3. data/lib/plant.rb +37 -46
  4. data/lib/query.rb +44 -0
  5. data/lib/reflection.rb +40 -0
  6. data/lib/selector.rb +181 -0
  7. data/lib/setup.rb +13 -22
  8. data/lib/stubber.rb +164 -21
  9. data/test/Rakefile.rb +11 -0
  10. data/test/app/models/address.rb +9 -0
  11. data/test/app/models/customer.rb +3 -0
  12. data/test/app/models/profile.rb +2 -0
  13. data/test/app/models/user.rb +7 -0
  14. data/test/databases.rake.rb +513 -0
  15. data/test/db/migrate/20101230223546_create_users.rb.rb +14 -0
  16. data/test/db/migrate/20101230223547_create_profiles.rb +15 -0
  17. data/test/db/migrate/20101230223548_create_customers.rb +11 -0
  18. data/test/db/migrate/20101230223549_create_addresses.rb +13 -0
  19. data/test/db/schema.rb +41 -0
  20. data/test/help_test.rb +15 -5
  21. data/test/plants.rb +5 -5
  22. data/test/test_basic_queries.rb +36 -0
  23. data/test/test_plant_definition.rb +129 -0
  24. data/test/test_plants_ids.rb +16 -0
  25. data/test/test_queries_on_has_many_association.rb +51 -0
  26. data/test/test_queries_on_has_one_association.rb +45 -0
  27. data/test/test_queries_on_model_attributes.rb +59 -0
  28. data/test/test_queries_with_like.rb +22 -0
  29. data/test/test_queries_with_limit.rb +28 -0
  30. data/test/test_queries_with_named_scope.rb +18 -0
  31. data/test/test_queries_with_order.rb +17 -0
  32. data/test/test_queries_with_ranges.rb +21 -0
  33. data/test/test_selector_condition.rb +26 -0
  34. data/test/test_stubbing.rb +43 -0
  35. metadata +60 -22
  36. data/test/models/adress.rb +0 -12
  37. data/test/models/customer.rb +0 -7
  38. data/test/models/profile.rb +0 -8
  39. data/test/models/user.rb +0 -8
  40. data/test/plant_tests.rb +0 -115
  41. data/test/test_plant.rb +0 -7
  42. data/test/test_plant_with_active_support.rb +0 -8
data/README.rdoc CHANGED
@@ -1,29 +1,64 @@
1
1
  == Overview
2
- Factory_boy aims to avoid slow unit tests due to usage of create/find fixtures in database, with factory_girl for example.
3
- Factory_boy can be used as factory_girl except that factories are not created in database.
4
- ActiveRecord::Base find method is stubbed to return fixtures (plants) you have instanciate.
5
- Differenciate find :all, :first, :last, but doesn't do the work of find with sql conditions.
6
- Anyway we don't think it's important for unit test, if some find calls with sql conditions, :include ... etc have to be
7
- tested, report it in integration tests is a better idea.
2
+ Factory Boy aims to avoid slow unit tests due to usage of create/find fixtures in database, with factory_girl for example.
3
+ Factory Boy can be used as factory_girl except that factories are not created in database.
4
+ ActiveRecord::Base finders method is stubbed to return fixtures (plants) you have instanciate.
5
+
6
+ Now, Factory Boy 2 handle stub of Active Record (3+) queries.
7
+ This means, the fixtures(plants) created with factory boy are retrieved via a AR queries(and only with AR new queries) of your models.
8
+ It does not pretend to stub 100% of all queries, but the coverage can be estimated at about 80%-90% of useful queries.
9
+
10
+
11
+ Active Record is stubbed only when at least one Plant is created in a test.
12
+ After each test everything is unstubbed.
13
+ That means, if you have a case where a particular(complex) query is executed but not right stubbed with factory boy you can test using fixtures in databases(with factory girl or just model.create ..), skipping factory boy process.
14
+
15
+ Tested with Active Record 3.0.1
16
+ Tests are suppose to use ActiveSupport::TestCase
8
17
 
9
18
  See some examples below.
10
- You can see also unit tests / code for deeper comprehension.
19
+ You should see unit tests to inspect tested stubbed queries!
11
20
 
12
- == Install
13
21
 
14
- gem install factory_boy
22
+ == Queries it supposes to handle
23
+
24
+ - where clauses on attributes and associations
25
+ - chained where clauses
26
+ - like sql predicate
27
+ - limit, offset
28
+ - order (with only one order clause)
29
+ - ranges (ie where(:age => (20..30)))
30
+ - IS NULL and IS NOT NULL sql predicates
31
+
32
+ The better way to see queries handled is to see all unit tests.
33
+
34
+ == Queries NOT handled
35
+
36
+ - Queries with explicit sql string(find_by_sql("..."))
37
+ - #order with more than one order clause (ie .order(name asc, age desc))
38
+ - IS and IS NOT with other operand than NULL
39
+
40
+ == Ids
15
41
 
42
+ Each plant fixture has now an (unique) id.
16
43
 
17
- == Basic Usage
44
+ == Usage
18
45
 
19
- Define your Plants (=~ Factories if factory_girl) in test/plants.rb
46
+ Define your Plants (~ Factories if factory_girl) in test/plants.rb
20
47
 
21
48
  Example :
22
49
 
50
+ Plant.define :address do |address|
51
+ address.number = 12
52
+ address.street = "rue de Brest"
53
+ end
54
+
23
55
  Plant.define :user do |user|
24
56
  user.name="Bart"
25
57
  user.age=800
58
+ user.addresses = [Plant(:address)]
26
59
  end
60
+
61
+
27
62
 
28
63
  Get it with :
29
64
 
@@ -34,10 +69,16 @@ Get it with :
34
69
 
35
70
 
36
71
  def test___1
37
- user = Plant(:user)
72
+ address = Plant(:address, :street => 'rue des Lilas')
73
+ user = Plant(:user, :name => 'Joe', :addresses => [address])
74
+
38
75
  assert_equal user, User.find #OK
39
76
  assert_equal user, User.find(:first) #OK
40
77
  assert_equal user, User.find(:last) #OK
78
+ assert_equal [user], User.where(:name => 'Joe') #OK
79
+ assert_equal [user], User.where("name = 'Joe' and addresses.street = 'rue des Lilas'").joins(':addresses) #OK
80
+ assert_equal [address], user.addresses.where(:street => 'rue des Lilas') #OK
81
+
41
82
  end
42
83
 
43
84
 
@@ -51,6 +92,7 @@ Get it with :
51
92
 
52
93
  user = Plant(:user, :name => "Marie", :age => age)
53
94
 
95
+
54
96
  == Specification of the class of the fixture definition
55
97
 
56
98
  Plant.define :admin, :class => User do |user|
@@ -66,16 +108,16 @@ Assign fixtures to association in definition of plant :
66
108
  profile.password = "BREIZH!"
67
109
  end
68
110
 
69
- Plant.define :adress do |adress|
70
- adress.number = 12
71
- adress.street = "rue de Brest"
111
+ Plant.define :address do |address|
112
+ address.number = 12
113
+ address.street = "rue de Brest"
72
114
  end
73
115
 
74
116
  Plant.define :user do |user|
75
117
  user.name = "Bart"
76
118
  user.age = 800
77
- user.profile = Plant.association(:profile)
78
- user.adresses = [Plant.association(:adress)]
119
+ user.profile = Plant(:profile)
120
+ user.adresses = [Plant(:address)]
79
121
  end
80
122
 
81
123
 
@@ -86,7 +128,7 @@ If you want to use the value of another attribute in definition, do like that :
86
128
 
87
129
  Plant.define :user do |user|
88
130
  user.name = "Marie"
89
- user.adresses = [Adress.new(:street => "Rue de #{user.name}")]
131
+ user.adresses = [Plant(:address, :street => "Rue de #{user.name}")]
90
132
  end
91
133
 
92
134
 
@@ -102,26 +144,21 @@ As with factory_girl you are able to use sequences, like that :
102
144
  Plant.next(:email) # => "incognito2@kantena.com"
103
145
 
104
146
 
105
- == Dependencies
106
-
107
- No dependency.
108
- Doesn't work, for now, with Active Support 3.0 (In development)
109
147
 
110
148
  == In Development
111
149
 
112
- - Stubs only find of Plant class so we can use both AR find on klasses that are not 'planted'
113
- - Plant(:xx).clear
114
- - Provide an id to each Plant
115
- - When assign instance to an association(ie profile of user), set foreign_key id (ie profile_id of user)
116
- - To work with Rails 3 (Active Support 3)
150
+ - Stubs aggregations methods in queries(sum, count ...)
117
151
 
118
- == Change Log
119
152
 
120
- FIX : Push new object in pool of a class doesn't override objects in pool. Safe push.
121
- FIX : Use #clone instead of #dup. Not same behavior with and without rails(may be overriden by active support)
153
+ == Install
154
+
155
+ gem install factory_boy
156
+
157
+
158
+ == Change Log
122
159
 
123
- == Notes
124
160
 
125
161
 
162
+ == Issues
126
163
 
127
164
  <b>Report Bugs here , on github</b>
@@ -0,0 +1,3 @@
1
+ class BlankSlate
2
+ instance_methods.each { |m| undef_method m unless m =~ /^__/ }
3
+ end
data/lib/plant.rb CHANGED
@@ -1,7 +1,8 @@
1
- require 'rubygems'
2
- require 'active_support/inflector'
3
1
  require 'stubber'
2
+ require 'selector'
4
3
  require 'setup'
4
+ require 'query'
5
+ require 'reflection'
5
6
 
6
7
  module Plant
7
8
 
@@ -9,83 +10,73 @@ module Plant
9
10
  @@pool = {}
10
11
  @@map = {}
11
12
  @@sequences = {}
12
- @@stubbed = []
13
-
13
+ @@id = 0
14
+
14
15
  def self.define symbol, args={}
15
16
  klass = args[:class] || symbol.to_s.camelize.constantize
16
- instance = klass.new
17
- yield instance if block_given?
18
- add_plant(klass, instance)
19
- add_plant(symbol, instance) if args[:class]
20
- stubs(instance.class)
21
- end
22
-
23
- def self.stubs klass
24
- unless @@stubbed.include?(klass)
25
- Plant::Stubber.stubs_find(klass)
26
- @@stubbed << klass
27
- end
17
+ definition = klass.new
18
+ yield definition if block_given?
19
+ add_plant(klass, definition)
20
+ add_plant(symbol, definition) if args[:class]
21
+ Plant::Reflection.reflect(klass)
22
+ Plant::Stubber.stubs
28
23
  end
29
-
30
- def self.unstub_find_for_each_class
31
- @@stubbed.each {|klass| Plant::Stubber.unstubs_find_for(klass)}
32
- @@stubbed = []
33
- end
34
-
35
- def self.all
36
- @@pool
37
- end
38
-
24
+
39
25
  def self.add_plant klass, instance
40
26
  @@plants[klass] = instance
41
27
  end
42
-
28
+
43
29
  def self.plants
44
30
  @@plants
45
31
  end
46
32
 
47
- def self.reload
33
+ def self.all
34
+ @@pool
35
+ end
36
+
37
+ def self.reload
48
38
  load "#{RAILS_ROOT}/test/plants.rb" if Plant.plants.empty?
49
39
  end
50
-
40
+
51
41
  def self.pool symbol
52
- instance = plants[symbol] || plants[symbol.to_s.camelize.constantize]
53
- object = instance.clone
42
+ definition = plants[symbol] || plants[symbol.to_s.camelize.constantize]
43
+ object = Plant::Reflection.clone(definition,@@id += 1)
54
44
  yield object if block_given?
55
- @@pool[instance.class] ||= []
56
- @@pool[instance.class] << object
45
+ @@pool[definition.class] ||= []
46
+ @@pool[definition.class] << object
57
47
  object
58
48
  end
59
-
49
+
60
50
  def self.destroy
61
51
  @@pool = {}
62
52
  @@plants = {}
63
53
  @@map = {}
64
54
  @@sequences = {}
65
- @@stubbed = []
55
+ @@id = 0
66
56
  end
67
-
57
+
68
58
  def self.sequence symbol, &proc
69
59
  @@sequences[symbol] = {:lambda => proc, :index => 0}
70
60
  end
71
-
61
+
72
62
  def self.next symbol
73
63
  @@sequences[symbol][:lambda].call(@@sequences[symbol][:index] += 1)
74
64
  end
75
65
 
76
- def self.association symbol
77
- Plant.pool(symbol)
66
+ def self.set_foreign_keys object, association, value
67
+ fk_setter = lambda {|value| value.send(Plant::Reflection.foreign_key(object, association) + '=', object.id)}
68
+
69
+ case
70
+ when Plant::Reflection.has_many_association?(object.class, association) then value.each {|v| fk_setter.call(v)}
71
+ when value && Plant::Reflection.has_one_association?(object.class, association) then fk_setter.call(value)
72
+ end
78
73
  end
79
-
74
+
80
75
  end
81
76
 
82
77
  def Plant symbol, args={}
83
- Plant.reload
78
+ Plant.reload
84
79
  Plant.pool(symbol) do |instance|
85
- args.each {|key, value| instance.send(key.to_s + '=', value)}
80
+ args.each {|key, value| Plant.set_foreign_keys(instance, key, value); instance.send(key.to_s + '=', value)}
86
81
  end
87
82
  end
88
-
89
-
90
-
91
-
data/lib/query.rb ADDED
@@ -0,0 +1,44 @@
1
+ module Plant
2
+ module Query
3
+
4
+ @@wheres = nil
5
+
6
+ def self.wheres= wheres
7
+ @@wheres = wheres
8
+ end
9
+
10
+ def self.wheres
11
+ @@wheres
12
+ end
13
+
14
+ def self.find_all klass
15
+ Plant.all[klass] || []
16
+ end
17
+
18
+ def self.find_by_ids klass, ids
19
+ plants = Plant.all[klass].select{|plant| ids.include?(plant.id)}
20
+ return plants.first if plants.size == 1
21
+ plants
22
+ end
23
+
24
+ def self.select klass
25
+ Plant::Selector.new(:klass => klass, :wheres => @@wheres, :plants => Plant.all[klass].to_a).select
26
+ end
27
+
28
+ def self.order objects, args
29
+ attribute, order = args.split(" ")
30
+ objects.sort {|x, y| (x,y = y,x if order == 'desc'); x.send(attribute.to_sym) <=> y.send(attribute.to_sym)}
31
+ end
32
+
33
+ def self.limit objects, limit_value
34
+ @@objects = objects
35
+ @@limit = limit_value
36
+ objects.first(limit_value)
37
+ end
38
+
39
+ def self.offset objects, offset_value
40
+ @@objects[offset_value..-1].first(@@limit)
41
+ end
42
+
43
+ end
44
+ end
data/lib/reflection.rb ADDED
@@ -0,0 +1,40 @@
1
+ module Plant
2
+ module Reflection
3
+ @@reflections = {}
4
+
5
+ def self.reflect klass
6
+ return if @@reflections[klass]
7
+ associations = lambda {|klass, macro| klass.reflect_on_all_associations(macro).map{|association| association.name} || []}
8
+
9
+ reflection = @@reflections[klass] = Hash.new
10
+ [:has_many, :has_one, :belongs_to].each {|relation| reflection[relation] = associations.call(klass, relation)}
11
+ reflection[:attributes] = klass.new.attributes
12
+ end
13
+
14
+ def self.clone object, id
15
+ clone = object.class.new
16
+ cloner = Proc.new {|attribute| clone.send(attribute.to_s + "=", object.send(attribute))}
17
+ reflection = @@reflections[object.class]
18
+
19
+ clone.id = id
20
+ reflection[:attributes].keys.each(&cloner)
21
+ [:has_many, :has_one, :belongs_to].each {|relation| reflection[relation].each(&cloner)}
22
+
23
+ clone
24
+ end
25
+
26
+ def self.has_many_association? klass, method
27
+ @@reflections[klass][:has_many].include?(method)
28
+ end
29
+
30
+ def self.has_one_association? klass, method
31
+ @@reflections[klass][:has_one].include?(method)
32
+
33
+ end
34
+
35
+ def self.foreign_key object, association
36
+ object.class.name.underscore + '_id'
37
+ end
38
+
39
+ end
40
+ end
data/lib/selector.rb ADDED
@@ -0,0 +1,181 @@
1
+ require 'blank_slate'
2
+
3
+ module Plant
4
+
5
+ class Selector
6
+
7
+ class Condition
8
+
9
+ def initialize wheres, klass
10
+ @wheres = wheres
11
+ @klass = klass
12
+ end
13
+
14
+ def to_ruby
15
+ @wheres.inject("") do |sql, where|
16
+ copy = where.clone
17
+
18
+ copy.gsub!(/\s=\s/, " == ")
19
+ copy.gsub!('"','')
20
+
21
+ copy.gsub!(/\s<>\s/, " != ")
22
+ copy.gsub!('"','')
23
+
24
+ copy.match(/(\sLIKE\s*)'/i)
25
+ copy.gsub!($1,'.match ') if $1
26
+
27
+ copy.match(/(\sIN\s*)\(/i)
28
+ copy.gsub!($1,'.included_in?') if $1
29
+
30
+ copy.match(/(\sBETWEEN\s*)((\d*)\sAND\s(\d*))/i)
31
+ between, range = $1, ($3.to_i .. $4.to_i)
32
+ copy.gsub!($2, '') if $2
33
+ copy.gsub!(between, ".included_in?(#{range}) ") if between
34
+
35
+ copy.match(/(\sIS\sNULL)/i)
36
+ copy.gsub!($1,'== nil') if $1
37
+
38
+ copy.match(/(\sIS\sNOT\sNULL)/i)
39
+ copy.gsub!($1,'!= nil') if $1
40
+
41
+
42
+ sql << (sql.blank? ? "" : " and ") + copy
43
+ end
44
+ end
45
+ end
46
+
47
+ class ArrayCollection < BlankSlate
48
+
49
+ def initialize collection
50
+ @collection = collection
51
+ end
52
+
53
+ def compare operator, operand
54
+ collection = @collection.dup
55
+ collection.map{|object| Attribute.new(object, @method)}.any? {|object| object.send(operator, operand) }
56
+ end
57
+
58
+ def method_missing method, *args, &block
59
+ if (@method)
60
+ return compare(method, *args)
61
+ end
62
+ @method = method
63
+ self
64
+ end
65
+ end
66
+
67
+ class Association
68
+
69
+ def initialize(association)
70
+ @association = association
71
+ end
72
+
73
+ def method_missing method, *args, &block
74
+ return nil unless @association
75
+ Attribute.new(@association, method)
76
+ end
77
+
78
+ end
79
+
80
+ class Attribute
81
+
82
+ def initialize reference, method=nil
83
+ @reference = reference
84
+ @method = method
85
+ end
86
+
87
+ def compare operator, operand
88
+ value = @reference.send(@method)
89
+ operand = type_cast(operand, value)
90
+ value.send(operator, operand)
91
+ end
92
+
93
+ def type_cast operand, value
94
+ case value
95
+ when TrueClass, FalseClass : (operand == 't' || operand == '1')
96
+ else operand
97
+ end
98
+ end
99
+
100
+ def == operand
101
+ compare(:==, operand)
102
+ end
103
+
104
+ def > operand
105
+ compare(:>, operand)
106
+ end
107
+
108
+ def < operand
109
+ compare(:<, operand)
110
+ end
111
+
112
+ def >= operand
113
+ compare(:>=, operand)
114
+ end
115
+
116
+ def <= operand
117
+ compare(:<=, operand)
118
+ end
119
+
120
+ def match operand
121
+ operand.gsub!(/\%/,'(.*)')
122
+ operand = '^' + operand + '$'
123
+ @reference.send(@method).match(operand)
124
+ end
125
+
126
+ def included_in? *operand
127
+ range = operand.first if operand.first.is_a?(Range)
128
+ return range.include?(@reference.send(@method)) if range
129
+ operand.include?(@reference.send(@method))
130
+ end
131
+
132
+ def method_missing method, *args, &block
133
+ @method = method
134
+ self
135
+ end
136
+
137
+ end
138
+
139
+ def initialize opt={}
140
+ @wheres = opt[:wheres]
141
+ @plants = opt[:plants]
142
+ @klass = opt[:klass]
143
+ end
144
+
145
+ def select
146
+ condition = Condition.new(@wheres, @klass)
147
+
148
+ Plant::Stubber.stubs_associations_collections
149
+ Plant::Stubber.stubs_attribute_methods
150
+ objects = @plants.select {|object| @binding = binding(); eval("#{condition.to_ruby}")}
151
+ Plant::Stubber.unstubs_associations_collections
152
+ Plant::Stubber.unstubs_attribute_methods
153
+
154
+ objects
155
+ end
156
+
157
+ private
158
+
159
+ def method_missing method, *args, &block
160
+ case
161
+ when has_one_association?(method) then Association.new(eval("object.#{method.to_s[0..-2]}", @binding))
162
+ when has_many_association?(method) then ArrayCollection.new(eval("object.#{method}", @binding))
163
+ when self_reference?(method) then Attribute.new(eval("object", @binding))
164
+ else Attribute.new(eval("object", @binding), method)
165
+ end
166
+ end
167
+
168
+ def self_reference? method
169
+ @klass.name.downcase == method.to_s[0..-2]
170
+ end
171
+
172
+ def has_one_association? method
173
+ @klass.new.respond_to?(method.to_s[0..-2])
174
+ end
175
+
176
+ def has_many_association? method
177
+ Plant::Reflection.has_many_association?(@klass, method)
178
+ end
179
+
180
+ end
181
+ end