factory_boy 1.0.5 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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