Rails doesn’t delete loaded fixtures in the tear down phase of tests, but it does delete and re-insert the fixtures you do use at the beginning of tests. This is a serious problem if you use foreign keys.
This issue is covered in the Rails issue tracker here: http://dev.rubyonrails.org/ticket/2404
Fixtures are YAML-format text files that hold test data. Your test cases can ask for fixtures to be loaded, and Rails will take care of putting them in the database during your tests’ setup phase.
Sadly, at no point are fixtures removed. Subsequent tests may reload the same fixtures (to assure that they’re intact), even if “transactional fixtures” (which really means transactional tests) are enabled. This reloading is slightly sloppy from a performance standpoint, but it’s a showstopper if you have foreign keys enabled, because the whole test class will fail.
Here’s an example: class A is referenced by B and C. The test for class B uses fixtures for A, B. The test for class C uses fixtures for A, C. When the fixtures for class C’s test are loaded, Rails does delete from A
and then reloads the fixtures for A, except the delete statement fails because there is still B fixture data referencing it. This data has nothing to do with the C test case’s needs, but it breaks the C test case.
Alternatives to solve this include:
- putting a
fixtures
line in test_helper.rb that loads all of your fixtures (so this will be included in every test class). The old data will be deleted and reloaded in the correct order so FKs aren’t violated, but lots of unnecessary data will be loaded for every test class… sloooow. - changing all of your foreign keys to
:deferrable => true
, so that the code in the big transaction that gets rolled back at the end of each test class never notices that the FKs were violated. This would be tedious to do and intrusive to your nice pretty migration code. Worse, it largely defeats the purpose of FKs, since your nice comprehensive test suite effectively runs with FKs disabled. - Not using fixtures, and using something else to load your test data. That might include explicit test data creation in your code (which is a pattern that fixtures are supposed to be a tidy implementation of), or a big SQL file full of insert statments that you execute at the beginning of your test suite.
- Monkeypatching Rails so that all fixtures that have been loaded are deleted when the test class has finished running. I don’t know how this would work; there might not be an easy hook to accomplish this. Also deleting and reloading the same fixture over and over if different test classes need it would be extra work that could theoretically be avoided.
- Monkeypatching Rails so that once a fixture is loaded, it is left alone (except for the beginning of the test suite when the database is dropped and recreated empty, as usual). This only works if “transactional fixtures” are enabled, since the test data is rolled back to its original loaded state at the end of each test. Since this is the default Rails 1.0+ behavior, it’s not a stretch to assume that this will usually work. The downside is that if you’re expecting that the database will be totally empty except for the fixtures you specifically asked for, you’ll be surprised by the fact that there’s other data in there. (But this is the current situation anyway; boy was I surprised!) To work around that minor issue, you could just make sure there are some records without dependent rows, so that regardless of whether the dependent table is loaded or not, the test expecting not to find dependent rows will pass.
This last option is the one that several people commenting on the bug suggested. Here’s the code I used, based on this comment from that bug.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
class Fixture attr_reader :class_name end class Fixtures @@inserted_fixture_list = {} alias original_insert_fixtures insert_fixtures def insert_fixtures return if 0 == values.size return if values.size > 0 && @@inserted_fixture_list[values[0].class_name] @@inserted_fixture_list[values[0].class_name] = true class_name = values[0].class_name if class_name.respond_to? "constantize" table_name = values[0].class_name.constantize.table_name else table_name = values[0].class_name.table_name end unless ActiveRecord::Base.connection.select_one("select 1 from #{table_name}") original_insert_fixtures end end def delete_existing_fixtures() end end |
It fixed the problem for me. I think that in order to avoid unexpected problems with fixture data being loaded when you didn’t ask for it in the current test, I may just preload all the fixtures in test_helper.rb anyway, and let the patch above be a performance optimization (since it would notice that the data was already there and not bother reloading it for subsequent tests).
Hope this helps!
”’Monkeypatching Rails so that all fixtures that have been loaded are deleted when the test class has finished running”’
in your test_helper.rb:
Thanks! I’m currently preloading all fixtures at the start of the test suite and letting Rails roll back the tests’ changes each time, but your code is helpful in case someone wants to do the per-test load-then-drop method.
Presumably if there are foreign keys then there would need to be some additional code that made the ordering correct.
Great… but it seems that this does not work completely. However when i put
‘self.use_transactional_fixtures = false’ it does.
So somehow everything is in a transaction and therefor cannot be deleted, which makes sense because you cannot delete something that is not there yet.
The problem i ran into next was the data created in my tests was still in the DB.
My first guess was to perform a rollback and close the transaction like you normally would and then call the delete_all. This worked for me
def teardown
ActiveRecord::Base.connection.rollback_db_transaction
Fixtures.all_loaded_fixtures.each do |table_name, fixtures|
klass = Module.const_get(table_name.to_s.classify)
klass.delete_all
end
end