Fixtures - all or nothing

I’m coming back to using fixtures some 10 years since I last touched them, and I think I have grossly misunderstood how they work. Take my Thing model, nothing special, a model with no attributes just a primary key

one: {}
two: {}

I also haven’t written anything without RSpec for 10 years but hey ho…

require "test_helper"

class ThingTest < ActiveSupport::TestCase
  fixtures :things

  test 'does a thing' do
    assert_equal things(:two), Thing.first
    assert_equal things(:one), Thing.last
    assert_equal 2, Thing.count
  end
end

class Thing2Test < ActiveSupport::TestCase
  test 'does a thing' do
    assert_equal nil, Thing.first
    assert_equal nil, Thing.last
    assert_equal 0, Thing.count
  end
end
# Running:

.F

Failure:
Thing2Test#test_does_a_thing [/Users/roblacey/repos/juniper/dungeon/test/models/thing_test.rb:15]:
--- expected
+++ actual
@@ -1 +1 @@
-nil
+#<Thing id: 298486374, created_at: "2023-05-17 11:54:38.297499000 +0000", updated_at: "2023-05-17 11:54:38.297499000 +0000">

It would appear that fixtures are always loaded. or they are loaded and then persist? I thought the fixtures were loaded into memory, then dumped into the database before each test and then rolled back with with each transaction. My real question here is, is there way that I can have fixtures for one test and not another? ATM I am not sure this is currently possible.

Many thanks

RobL

The fixtures API does indicate that fixtures are loaded always by default:

The testing environment will automatically load all the fixtures into the database before each test. To ensure consistent data, the environment deletes the fixtures before running the load.

That page has more details on other options that control how the fixtures work. The docs for the fixtures method are nonexistent and it’s not clear to me what that does.

I don’t use fixtures often, but have tried recently (I gave up because I just cannot get them to work reliably and don’t want to use them bad enough to debug it)

1 Like

Thanks @davetron5000. I’ve just spend the past two days trying to debug how it works. This is what I can fathom.

Fixtures don’t get loaded automatically as such. However… ActiveRecord::Fixtures overrides the default before_setup to call setup_fixtures. setup_fixtures loads the fixtures that you have setup with fixtures :users, :things # or :all. So as long as you’ve defined the fixtures to load, they will be loaded when the first test is run that has fixtures defined. I am looking at this using RSpec so it might be slightly different to Test::Unit.

In this case the first test would run, then the second which loads fixtures kinda lazily before the before block.

RSpec.describe SomeThing do
  it 'does something' do
    expect(true).to be_true
  end
end

RSpec.describe Thing do
  fixtures :all

  before do
    # fixtures are loaded before here
  end

  it 'does something' do
    expect(true).to be_true
  end
end

Once loaded those fixtures hang around. The fixtures are loaded and then the transaction for the individual test is started and rolled back at the end of the test leaving the fixture data as it was before the individual test started. The problem I’ve seen with this is that the fixture data bleeds into subsequent tests. So having a situation where you want to have some fixtures in one spec and none in another is something you can’t rely on. This isn’t necessarily what I expected. And the fixture data bleeding into other tests is really annoying. In this case I’ve managed to mitigate it (for now) with forcing the order of our rspec tests to push our fixture tests (specs) to the back of the queue. I need to tweak this to still allow randomised ordering before partitioning.

  config.register_ordering :global do |examples|
    fixtures, other = examples.partition do |example|
      example.metadata[:fixtures].present?
    end
    other + fixtures
  end

Unfortunately I still need to find a solution to loading multiple different fixtures groups and persisting the fixtures most efficiently. I have an idea of what to do vaguely. But it might just be grouping specs and ensuring the database is truncated at the end of each group run which is fiddly but possible.

RobL

1 Like

Unfortunately I still need to find a solution to loading multiple different fixtures groups

Are you you need different fixture groups?

Typically, fixtures will be a predefined set of records that are loaded at the start of your test run and deleted after it. This happens very fast and allows you to write tests/specs that assume those same fixtures are always there. Normally, a test helper file will just load all fixtures so you don’t have to think about it; if you’re running a spec the fixtures are always there.

# /spec/rails_helper.rb

RSpec.configure do |config|
  config.use_transactional_fixtures = true
  config.global_fixtures = :all
end

When using fixtures, you generally want to avoid doing things like counting the absolute number of records in the database, because you might add another fixture and then your counts would break.

Instead, try to assert (or expect) changes to occur when you do something, for example:

RSpec.describe Thing do
  it "can be created" do
    thing = Thing.new
    expect { thing.save }.to change { Thing.count }.by(1)
  end
end

This will pass whether you have 0 Thing fixtures or 1 or 100.

1 Like

Oh I agree @moveson we need to write tests under the assumption that the fixture data could always there. I needed to roll out something quickly and not get distracted fixing every test that hadn’t previously assumed that.

I mean you’re right I don’t necessarily need different groups in the context of running the test suite, but with different teams working on different sets of specs / features we felt that having a division somewhere is helpful. However, maybe we just need to cope with that since it’s not like in the real world we don’t just have a single set of data in the wild. Yes, that’s decided then :stuck_out_tongue:

1 Like

This sounds like a bug.

So as long as you’ve defined the fixtures to load, they will be loaded when the first test is run that has fixtures defined.

They should be deleted after each test class (or more accurately the transaction that adds them should be rolled back).

1 Like

Nevermind, I read the code again. It looks like if you don’t call fixtures then the helper methods (eg. topics(:foo)) aren’t defined within that class. But the full fixture set is always loaded into the database eventually.

I don’t think this is correct, but I will investigate more.

1 Like
class OverlappingFixturesTest < ActiveRecord::TestCase
  fixtures :topics, :developers
  fixtures :developers, :accounts

  def test_fixture_table_names
    assert_equal %w(topics developers accounts), fixture_table_names
    assert_not_nil topics(:first)
  end
end

class NoFixturesTest < ActiveRecord::TestCase
  def test_no_fixtures_defined_in_this_class
    assert_equal([], fixture_table_names)
    assert_raise NoMethodError do
      topics(:first)
    end
    assert_nil Topic.first
  end
end

The test order matters here, but if NoFixturesTest runs second, then assert_nil Topic.first will fail because a Topic will exist (from OverlappingFixturesTest).

1 Like

So yeah, the fixtures hang around in the database, and the transaction takes care of rolling back any changes / new additions that ensures the tests are fast. But also annoying because to be efficient you should only keep the fixtures for as long as you need them, and grouping by batches of tests that require fixtures and dumping at the end of that batch is the only way to achieve this. I will point out the fixture set I am dealing with is a few hundred Mb so loading it is not a trivial amount of time.

1 Like

1 Like

i’m working with a legacy application that relied on rails 4 behavior whereby fixtures did not stick around for subsequent spec files. so i hacked together a solution (using rspec) which seems to maintain this old behavior: only specs which explicitly declare fixtures :all (or whatever fixtures you want) will have those fixtures loaded in the DB for the duration of that file. in (rails|spec)_helper.rb RSpec.configure block:

config.after :all do
  conn = ActiveRecord::Base.connection
  fixture_table_names.each do |table_name|
    conn.delete "DELETE FROM #{conn.quote_table_name(table_name)}", "Fixture Delete"
  end
  ActiveRecord::FixtureSet.reset_cache
  invalidate_already_loaded_fixtures # needed for rails 7.2.2
end

why isn’t this a built-in feature? it’s probably a little slower than the original behavior, but seems super useful. am i missing something?