jeremyevans-fixture_dependencies 1.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.
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
+