Politics, Programming and Possibilities
26 May
If any of the debates at the Mountain West RubyConf last March could be called “heated”, I’d say the hottest one came when we started to discuss unit tests and functional tests in Rails. I might be reading my own story into things, but what I got out of the discussion was that there is a consensus surrounding the idea that unit tests are good, and we’re all glad that Rails ships with a testing suite, but the brittleness of testing with fixtures is something that has yet to be solved universally.
The Flexible Solution, Redux
I’ve approached this problem before and offered a “flexible fixtures” patch (twice) to the core team that never made it in. (It’s ok, I’m not bitter… I just think it’s a higher priority than they do). With that patch having long been laid aside, I offer here a simple fix that will add much of the same functionality with a small change (fewer than 20 lines of reasonable length) to your test_helper.rb file (see below).
What you get is the ability to specify the directory (i.e. the “context”) from which to load certain fixtures. For example, you might have several fixtures that are always loaded (e.g. drop-down box data or other basic data that is required just for your app to run) and then several scenarios or contexts that rely on that base level of data.
This is how your unit test would look using this “context_fixtures” call:
context_fixtures "base", :cities, :countries, :holidays
context_fixtures "three_clients", :users, :accounts, :transactions
# ... etc.
end
# ... etc.
end
You can use context_fixtures in your functional tests this way, too. I organize my test/fixtures directory into subfolders such as test/fixtures/base, and test/fixtures/three_clients, etc. Note that this solution is not as flexible as the “flexible fixtures” patch, since it still requires that the fixture files map one-to-one to the tables in your database. As usual, however, it’s ok to choose not to load data into some tables.
Keeping Your Deprecated Fixtures Around
If you’re in the middle of a project and you realize that you need these contexts in order to reduce the complexity of writing tests, what do you do with the test cases and data that are already working just wonderfully? You don’t want to have to discard those. Instead, you could create your own custom TestCase classes, like this:
self.fixture_path =
File.join(RAILS_ROOT,
"test", "deprecated_fixtures")
end
end
end
self.fixture_path =
File.join(RAILS_ROOT,
"test", "deprecated_fixtures")
end
end
After adding the above code to your test_helper.rb file,
It took me just a minute or two to search and replace “TestCase” with “DeprecatedFixturesTestCase”. With that change, from here on out, just use the regular TestCase class for your new tests and use “context_fixtures” instead of “fixtures”.
Context Fixture Source Code for “test_helper.rb”
= "test"
env = File.dirname(__FILE__) + "/../config/environment"
# Add a little more granularity to our fixture
# directories so that we can load some fixtures from
# one directory (context), and other fixtures from
# other directories.
superclass_delegating_accessor :contexts
self.contexts = {}
alias context_free_initialize initialize
if contexts.has_key?(table_name.to_s)
fixture_path =
File.join(RAILS_ROOT, "test", "fixtures",
contexts[table_name.to_s],
table_name.to_s)
end
context_free_initialize(
connection, table_name,
class_name, fixture_path,
file_filter)
end
end
self.use_transactional_fixtures = false
self.use_instantiated_fixtures = false
# Provide the 'context_fixtures' class method
# in conjunction with our modded Fixtures class
# above.
fxtrs.each do |f|
Fixtures.contexts[f.to_s] = context
end
fixtures *fxtrs
end
end
end
end
Tags: ruby, rails, fixtures, unit tests
6 Responses for "Simplifying Rails Fixtures with Contexts"
Thanks for this. I inherited a project using fixture_scenarios and I wanted to move it to rails 2.1. Unfortunately fixture_scenarios isn’t quite ready for 2.1, but your code worked well. Cheers.
/g
@George: Glad this worked for you too
Wondering if you’re using autotest with this code. The tests run successfully individually, but I’m getting the following when running them under autotest:
SystemStackError: stack level too deep
(eval):9:in `contexts'
./test/unit/../test_helper.rb:87:in `context_free_initialize'
./test/unit/../test_helper.rb:91:in `context_free_initialize'
./test/functional/../test_helper.rb:91:in `initialize'
Have you run into this?
@George: I’m not using autotest, but it would be great if we could figure out what’s going wrong. I wonder if autotest loads the helper file twice? Maybe you could put some kind of guard code around the method aliasing (e.g. unless const_defined? :LOADED; alias context_free_initialize initialize; LOADED = true; end)
@Duane: the guard code seems to help a bit, but it’s not the complete answer. To wit:
WITHOUT guard code:
112 tests, 1 assertions, 0 failures, 111 errors
These are primarily stack level errors:
SystemStackError: stack level too deep
which prevent the assertions from executing.
WITH guard code:
112 tests, 130 assertions, 6 failures, 45 errors
No more “stack level too deep” errors, but fixture data isn’t being initialized so I’m getting errors along the lines of:
StandardError: No fixture with name 'update' found for table 'articles'
So now I need to figure out why the fixture data isn’t being loaded when the guard code is in place.
Here’s an example from an autotest run with the guard code in place: http://pastie.org/230059
Hello,
Thanks for this “patch”. This is very useful.
@George, did you find how to make it work with autotest?
Thanks
Nicolas
Leave a reply