jeremyevans-fixture_dependencies 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2007 Jeremy Evans
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
data/README ADDED
@@ -0,0 +1,192 @@
1
+ fixture_dependencies is a plugin that changes the way Rails uses fixtures in
2
+ the following ways:
3
+
4
+ - Fixtures can specify associations instead of foreign keys
5
+ - Supports belongs_to, has_many, has_one, and habtm associations
6
+ - Loads a fixture's dependencies (associations with other fixtures) before the
7
+ fixture itself so that foreign key constraints aren't violated
8
+ - Can specify individual fixtures to load per test or test suite
9
+ - Loads fixtures on every test inside a transaction, so fixture information
10
+ is never left in your database
11
+ - Handles almost all cyclic dependencies
12
+
13
+ To use, first install the plugin, then add the following to
14
+ test/test_helper.rb after "require 'test_help'":
15
+
16
+ require 'fixture_dependencies_test_help'
17
+
18
+ This overrides the default test helper to load the fixtures inside transactions
19
+ and to use FixtureDependencies to load the fixtures.
20
+
21
+ fixture_dependencies is available via github:
22
+
23
+ http://github.com/jeremyevans/fixture_dependencies/tree/master
24
+
25
+ Changes to Fixtures:
26
+
27
+ fixture_dependencies is designed to require the least possible changes to
28
+ fixtures. For example, see the following changes:
29
+
30
+ OLD NEW
31
+ asset1: asset1:
32
+ id: 1 id: 1
33
+ employee_id: 2 employee: jeremy
34
+ product_id: 3 product: nx7010
35
+ vendor_id: 2 vendor: lxg_computers
36
+ note: in working order note: in working order
37
+
38
+ As you can see, you just replace the foreign key attribute and value with the
39
+ name of the association and the associations name. This assumes you have an
40
+ employee fixture with a name of jeremy, and products fixture with the name of
41
+ nx7010, and a vendors fixture with the name lxg_computers.
42
+
43
+ Fixture files still use the table_name of the model.
44
+
45
+ Changes to the fixtures Class Method:
46
+
47
+ fixture_dependencies can still use the fixtures class method in your test:
48
+
49
+ class EmployeeTest < Test::Unit::TestCase
50
+ fixtures :assets
51
+ end
52
+
53
+ In Rails default testing practices, the arguments to fixtures are table names.
54
+ fixture_dependencies changes this to underscored model names. If you are using
55
+ Rails' recommended table practices, this shouldn't make a difference.
56
+
57
+ Another change is that Rails defaults allow you to specify habtm join tables in
58
+ fixtures. That doesn't work with fixture dependencies, as there is no
59
+ associated model. Instead, you use a has_and_belongs_to_many association name
60
+ in the the appropriate model fixtures (see below).
61
+
62
+ Loading Individual Fixtures with fixtures class method:
63
+
64
+ There is support for loading individual fixtures (and just their dependencies),
65
+ using the following syntax:
66
+
67
+ class EmployeeTest < Test::Unit::TestCase
68
+ fixtures :employee__jeremy # Note the double underscore
69
+ end
70
+
71
+ This would load just the jeremy fixture and its dependencies. I find this is
72
+ much better than loading all fixtures in most of my test suites.
73
+
74
+ Loading Fixtures Inside Test Methods:
75
+
76
+ I find that it is often better to skip the use of the fixtures method entirely,
77
+ and load the fixtures I want manually in each test method. This provides for
78
+ the loosest coupling possible. Here's an example:
79
+
80
+ class EmployeeTest < Test::Unit::TestCase
81
+ def test_employee_name
82
+ # Load the fixture and return the Employee object
83
+ employee = load(:employee__jeremy)
84
+ # Test the employee
85
+ end
86
+
87
+ def test_award_statistics
88
+ # Load all fixtures in both tables
89
+ load(:employee_awards, :awards)
90
+ # Test the award_statistics method
91
+ # (which pulls data from the tables loaded above)
92
+ end
93
+ end
94
+
95
+ Don't worry about loading the same fixture twice, if a fixture is already
96
+ loaded, it won't attempt to load it again.
97
+
98
+ has_* Assocations in Fixtures:
99
+
100
+ Here's an example of using has_one (logon_information), has_many (assets), and
101
+ has_and_belongs_to_many (groups) associations.
102
+
103
+ jeremy:
104
+ id: 2
105
+ name: Jeremy Evans
106
+ logon_information: jeremy
107
+ assets: [asset1, asset2, asset3]
108
+ groups: [group1]
109
+
110
+ logon_information is a has_one association to another table which was split
111
+ from the employees table due to database security requirements. Assets is a
112
+ has_many association, where one employee is responsible for the asset.
113
+ Employees can be a member of multiple groups, and each group can have multiple
114
+ employees.
115
+
116
+ For has_* associations, after fixture_dependencies saves jeremy, it will load
117
+ and save logon_information (and its dependencies...), it will load each asset
118
+ in the order specified (and their dependencies...), and it will load all of the
119
+ groups in the order specified (and their dependencies...). Note that there
120
+ is only a load order inside a specific association, associations are stored
121
+ in the same hash as attributes and are loaded in an arbitrary order.
122
+
123
+ Cyclic Dependencies:
124
+
125
+ fixture_dependencies handles almost all cyclic dependencies. It handles all
126
+ has_many, has_one, and habtm cyclic dependencies. It handles all
127
+ self-referential cyclic dependencies. It handles all belongs_to cyclic
128
+ dependencies except the case where there is a NOT NULL or validates_presence of
129
+ constraint on the cyclic dependency's foreign key.
130
+
131
+ For example, a case that won't work is when employee belongs_to supervisor
132
+ (with a NOT NULL or validates_presence_of constraint on supervisor_id), and
133
+ john is karl's supervisor and karl is john's supervisor. Since you can't create
134
+ john without a valid supervisor_id, you need to create karl first, but you
135
+ can't create karl for the same reason (as john doesn't exist yet).
136
+
137
+ There isn't a generic way to handle the belongs_to cyclic dependency, as far as
138
+ I know. Deferring foreign key checks could work, but may not be enabled (and
139
+ one of the main reasons to use the plugin is that it doesn't require them).
140
+ For associations like the example above (employee's supervisor is also an
141
+ employee), setting the foreign_key to the primary key and then changing it
142
+ later is an option, but database checks may prevent it. For more complex
143
+ cyclic dependencies involving multiple model classes (employee belongs_to
144
+ division belongs_to head_of_division when the employee is a member of the
145
+ division and also the head of the division), even that approach is not
146
+ possible.
147
+
148
+ Known Issues:
149
+
150
+ Currently, the plugin only supports yaml fixtures, but other types of fixtures
151
+ would be fairly easy to add (send me a patch if you add support for another
152
+ fixture type).
153
+
154
+ The plugin is significantly slower than the default testing method, because it
155
+ loads all fixtures inside of a transaction (one per test method), where Rails
156
+ defaults to loading the fixtures once per test suite (outside of a
157
+ transaction), and only deletes fixtures from a table when overwriting it with
158
+ new fixtures. Rails actually did something similar starting with r2714, but it
159
+ was rolled back in r2730 due to speed issues. See ticket #2404 on Rails' trac.
160
+
161
+ Instantiated fixtures are not available with this plugin. Instead, you should
162
+ use load(:model__fixture_name).
163
+
164
+ Troubleshooting:
165
+
166
+ If you run into problems with loading your fixtures, it can be difficult to see
167
+ where the problems are. To aid in debugging an error, add the following to
168
+ test/test_helper.rb:
169
+
170
+ FixtureDependencies.verbose = 2
171
+
172
+ This will give a verbose description of the loading and saving of fixtures for
173
+ every test, including the recursive loading of all dependencies.
174
+
175
+ Similar Ideas:
176
+
177
+ fixture_references is a similar plugin. It uses erb inside yaml, and uses the
178
+ foreign key numbers inside of the association names, which leads me to believe
179
+ it doesn't support has_* associations.
180
+
181
+ Ticket #6424 on the Rails' trac also implements a similar idea, but it parses
182
+ the associations and changes them to foreign keys, which leads me to believe it
183
+ doesn't support has_* associations either.
184
+
185
+ License:
186
+
187
+ fixture_dependencies is released under the MIT License. See the LICENSE file
188
+ for details.
189
+
190
+ Author:
191
+
192
+ Jeremy Evans <code@jeremyevans.net>
@@ -0,0 +1,157 @@
1
+ class FixtureDependencies
2
+ @fixtures = {}
3
+ @loaded = {}
4
+ @verbose = 0
5
+ class << self
6
+ attr_reader :fixtures, :loaded
7
+ attr_accessor :verbose
8
+ # Load all record arguments into the database. If a single argument is
9
+ # given and it corresponds to a single fixture, return the the model
10
+ # instance corresponding to that fixture. If a single argument if given
11
+ # and it corresponds to a model, return all model instances corresponding
12
+ # to that model. If multiple arguments are given, return a list of
13
+ # model instances (for single fixture arguments) or list of model instances
14
+ # (for model fixture arguments). If no arguments, return the empty list.
15
+ #
16
+ # This will load the data from the yaml files for each argument whose model
17
+ # is not already in the fixture hash.
18
+ def load(*records)
19
+ ret = records.collect do |record|
20
+ model_name, name = split_name(record)
21
+ if name
22
+ use(record.to_sym)
23
+ else
24
+ model_name = model_name.singularize
25
+ unless loaded[model_name.to_sym]
26
+ puts "loading #{model_name}.yml" if verbose > 0
27
+ load_yaml(model_name)
28
+ end
29
+ fixtures[model_name.to_sym].keys.collect{|name| use("#{model_name}__#{name}".to_sym)}
30
+ end
31
+ end
32
+ records.length == 1 ? ret[0] : ret
33
+ end
34
+
35
+ private
36
+ # Add a fixture to the fixture hash (does not add to the database,
37
+ # just makes it available to be add to the database via use).
38
+ def add(model_name, name, attributes)
39
+ (fixtures[model_name.to_sym]||={})[name.to_sym] = attributes
40
+ end
41
+
42
+ # Get the model instance that already exists in the database using
43
+ # the fixture name.
44
+ def get(record)
45
+ model_name, name = split_name(record)
46
+ model = model_name.camelize.constantize
47
+ model.find(fixtures[model_name.to_sym][name.to_sym][model.primary_key.to_sym])
48
+ end
49
+
50
+ # Adds all fixtures in the yaml fixture file for the model to the fixtures
51
+ # hash (does not add them to the database, see add).
52
+ def load_yaml(model_name)
53
+ YAML.load(File.read(File.join(Test::Unit::TestCase.fixture_path, "#{model_name.camelize.constantize.table_name}.yml"))).each do |name, attributes|
54
+ symbol_attrs = {}
55
+ attributes.each{|k,v| symbol_attrs[k.to_sym] = v}
56
+ add(model_name.to_sym, name, symbol_attrs)
57
+ end
58
+ loaded[model_name.to_sym] = true
59
+ end
60
+
61
+ # Split the fixture name into the name of the model and the name of
62
+ # the individual fixture.
63
+ def split_name(name)
64
+ name.to_s.split('__', 2)
65
+ end
66
+
67
+ # Load the individual fixture into the database, by loading all necessary
68
+ # belongs_to dependencies before saving the model, and all has_*
69
+ # dependencies after saving the model. If the model already exists in
70
+ # the database, return it. Will check the yaml file for fixtures if no
71
+ # fixtures yet exist for the model. If the fixture isn't in the fixture
72
+ # hash, raise an error.
73
+ def use(record, loading = [], procs = {})
74
+ spaces = " " * loading.length
75
+ puts "#{spaces}using #{record}" if verbose > 0
76
+ puts "#{spaces}load stack:#{loading.inspect}" if verbose > 1
77
+ loading.push(record)
78
+ model_name, name = split_name(record)
79
+ model = model_name.camelize.constantize
80
+ unless loaded[model_name.to_sym]
81
+ puts "#{spaces}loading #{model.table_name}.yml" if verbose > 0
82
+ load_yaml(model_name)
83
+ end
84
+ raise ActiveRecord::RecordNotFound, "Couldn't use fixture #{record.inspect}" unless attributes = fixtures[model_name.to_sym][name.to_sym]
85
+ # return if object has already been loaded into the database
86
+ if existing_obj = model.send("find_by_#{model.primary_key}", attributes[model.primary_key.to_sym])
87
+ return existing_obj
88
+ end
89
+ obj = model.new
90
+ many_associations = []
91
+ attributes.each do |attr, value|
92
+ if reflection = model.reflect_on_association(attr.to_sym)
93
+ if reflection.macro == :belongs_to
94
+ dep_name = "#{reflection.klass.name.underscore}__#{value}".to_sym
95
+ if dep_name == record
96
+ # Self referential record, use primary key
97
+ puts "#{spaces}#{record}.#{attr}: belongs_to self-referential" if verbose > 1
98
+ attr = reflection.options[:foreign_key] || reflection.klass.table_name.classify.foreign_key
99
+ value = attributes[model.primary_key.to_sym]
100
+ elsif loading.include?(dep_name)
101
+ # Association cycle detected, set foreign key for this model afterward using procs
102
+ # This is will fail if the column is set to not null or validates_presence_of
103
+ puts "#{spaces}#{record}.#{attr}: belongs-to cycle detected:#{dep_name}" if verbose > 1
104
+ (procs[dep_name] ||= []) << Proc.new do |assoc|
105
+ m = model.find(attributes[model.primary_key.to_sym])
106
+ m.send("#{attr}=", assoc)
107
+ m.save!
108
+ end
109
+ value = nil
110
+ else
111
+ # Regular assocation, load it
112
+ puts "#{spaces}#{record}.#{attr}: belongs_to:#{dep_name}" if verbose > 1
113
+ use(dep_name, loading, procs)
114
+ value = get(dep_name)
115
+ end
116
+ elsif
117
+ many_associations << [attr, reflection, reflection.macro == :has_one ? [value] : value]
118
+ next
119
+ end
120
+ end
121
+ obj.send("#{attr}=", value)
122
+ end
123
+ puts "#{spaces}saving #{record}" if verbose > 1
124
+ obj.save!
125
+ loading.pop
126
+ # Update the circular references
127
+ if procs[record]
128
+ procs[record].each{|p| p.call(obj)}
129
+ procs.delete(record)
130
+ end
131
+ # Update the has_many and habtm associations
132
+ many_associations.each do |attr, reflection, values|
133
+ proxy = obj.send(attr)
134
+ values.each do |value|
135
+ dep_name = "#{reflection.klass.name.underscore}__#{value}".to_sym
136
+ if dep_name == record
137
+ # Self referential, add association
138
+ puts "#{spaces}#{record}.#{attr}: #{reflection.macro} self-referential" if verbose > 1
139
+ reflection.macro == :has_one ? (proxy = obj) : (proxy << obj)
140
+ elsif loading.include?(dep_name)
141
+ # Cycle Detected, add association to this object after saving other object
142
+ puts "#{spaces}#{record}.#{attr}: #{reflection.macro} cycle detected:#{dep_name}" if verbose > 1
143
+ (procs[dep_name] ||= []) << Proc.new do |assoc|
144
+ reflection.macro == :has_one ? (proxy = assoc) : (proxy << assoc unless proxy.include?(assoc))
145
+ end
146
+ else
147
+ # Regular association, add it
148
+ puts "#{spaces}#{record}.#{attr}: #{reflection.macro}:#{dep_name}" if verbose > 1
149
+ assoc = use(dep_name, loading, procs)
150
+ reflection.macro == :has_one ? (proxy = assoc) : (proxy << assoc unless proxy.include?(assoc))
151
+ end
152
+ end
153
+ end
154
+ obj
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,34 @@
1
+ module Test
2
+ module Unit
3
+ class TestCase
4
+ class << self
5
+ alias_method :stupid_method_added, :method_added
6
+ end
7
+ def self.method_added(x)
8
+ end
9
+
10
+ # Load fixtures using FixtureDependencies inside a transaction
11
+ def setup_with_fixtures
12
+ ActiveRecord::Base.send :increment_open_transactions
13
+ ActiveRecord::Base.connection.begin_db_transaction
14
+ load_fixtures
15
+ end
16
+ alias_method :setup, :setup_with_fixtures
17
+
18
+ class << self
19
+ alias_method :method_added, :stupid_method_added
20
+ end
21
+
22
+ private
23
+ # Load fixtures named with the fixtures class method
24
+ def load_fixtures
25
+ load(*fixture_table_names)
26
+ end
27
+
28
+ # Load given fixtures using FixtureDependencies
29
+ def load(*fixture)
30
+ FixtureDependencies.load(*fixture)
31
+ end
32
+ end
33
+ end
34
+ end
metadata ADDED
@@ -0,0 +1,59 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jeremyevans-fixture_dependencies
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Jeremy Evans
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-07-06 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description:
17
+ email: code@jeremyevans.net
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files:
23
+ - LICENSE
24
+ files:
25
+ - README
26
+ - LICENSE
27
+ - lib/fixture_dependencies.rb
28
+ - lib/fixture_dependencies_test_help.rb
29
+ has_rdoc: true
30
+ homepage:
31
+ post_install_message:
32
+ rdoc_options:
33
+ - --inline-source
34
+ - --line-numbers
35
+ - README
36
+ - lib
37
+ require_paths:
38
+ - lib
39
+ required_ruby_version: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: "0"
44
+ version:
45
+ required_rubygems_version: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: "0"
50
+ version:
51
+ requirements: []
52
+
53
+ rubyforge_project:
54
+ rubygems_version: 1.2.0
55
+ signing_key:
56
+ specification_version: 2
57
+ summary: Rails fixture loading that works with foreign keys
58
+ test_files: []
59
+