DEV Community

Augusts Bautra
Augusts Bautra

Posted on

Bug in `use_transactional_fixtures`

tl;dr

As late as rspec-rails v7 vanilla transactional tests with use_transactional_fixtures = true have an unfortunate bug where quitting out of an example (binding.pry + !!!) does not end the example's transaction. This will interfere with any cleanup of records written to DB outside transactions in after(:all), after(:suite), and even at_exit. Consider using DatabaseCleaner-powered transactions instead, those close correctly.

The Story

I've been working on lowering the time it takes to run UPB system's CI. There are many little things one can do (caching CI dependencies, configuring parallel runners to use parallel_runtime_rspec.log correctly etc.), but one of the more impactful techniques tends to be using TestProf to identify any hotspots and addressing those.

In this case I identified that specs tend to require lots of project records to spec things in them. Many systems have these "everything else depends on these" records, like User.

A possible solution for this situation is to use AnyFixture to "seed" some generic records outside transactions, usually at suite start, and reuse them in transactional specs. The nice thing about this setup is that any changes to these "canonical" records will be rolled back at end of transactional specs, but the downside is that specs can no longer rely on the database being completely empty - .to change { Thing.count }.from(0).to(1) will have to be repaced with relative asserts .to change { Thing.count }.by(1).

Anywho, I prepared a MR and CI passed just fine, and we merged the changes, but starting work on new feature I encountered weird spec failures regarding uniqueness constraint violations - some records were there at suite start!

This had me baffled because I had made sure AnyFixture was cleaning itself up, but in some cases apparently it did not.

Several long hours of hair-pulling later I realized that the issue occurs if I quit out of an example. This is easy to reproduce:

Set up after-suite hooks

config.after(:all) do
  binding.pry
end

config.after(:suite) do
  binding.pry
end

at_exit do
  binding.pry
end
Enter fullscreen mode Exit fullscreen mode

And hook into en example, before block also works:

before { binding.pry }
Enter fullscreen mode Exit fullscreen mode

Run the example, and exit from the pry session in before hook normally (with exit), and run ActiveRecord::Base.connection.transaction_open? in all after hooks. It will be false.

Now run the example again, but this time quit out of the example with !!!. All after hooks will report a transaction being open.
Any cleanup in after hooks will occur in this still-open example transaction and will never got committed, leaving a dirty DB.

The workaround I've found for now is to turn vanilla transactionality off, and use DatabaseCleaner-powered transactions instead, those close correctly:

config.use_transactional_fixtures = false

DatabaseCleaner.strategy = :transaction

config.around do |example|
  DatabaseCleaner.cleaning { example.run }
end
Enter fullscreen mode Exit fullscreen mode

Top comments (0)