Skip to content

Instantly share code, notes, and snippets.

@ghiculescu
Created June 2, 2021 14:08
Show Gist options
  • Save ghiculescu/d27c01638514a533704ca0391293453d to your computer and use it in GitHub Desktop.
Save ghiculescu/d27c01638514a533704ca0391293453d to your computer and use it in GitHub Desktop.
Optimizing Rails fixtures

I had a thought while riding to work today: how much faster would CI and tests be if we had a lot less fixtures. I had a suspicion that cutting down on our fixtures load would make CI a lot faster.

Let's test this hypothesis:

Given this test class:

class SomethingTest < ActiveSupport::TestCase
  test "something" do
  end
end

When I run Benchmark.ms { test('unit/something') }, I see this:

Tanda @ Rails 6.0.3.7 (test) :007 > Benchmark.ms { test('unit/something') }
Run options: --seed 62378

# Running:

-----------------------------
SomethingTest: test_something
-----------------------------
.

Finished in 0.965725s, 1.0355 runs/s, 0.0000 assertions/s.

1 runs, 0 assertions, 0 failures, 0 errors, 0 skips
3346.742656896822

ie. there's about 2.5s of unaccounted for time. And also, there's nearly 1s of test time not spent inside the test (we assume, since the test does nothing).

If I delete all the fixtures so that there's no fixtures for Rails to load, I get this:

Tanda @ Rails 6.0.3.7 (test) :008 > Benchmark.ms { test('unit/something') }
Run options: --seed 47190

# Running:

-----------------------------
SomethingTest: test_something
-----------------------------
.

Finished in 0.010474s, 95.4748 runs/s, 0.0000 assertions/s.

1 runs, 0 assertions, 0 failures, 0 errors, 0 skips
2046.133188996464

So it seems there's consistently around 900ms added to each test in loading fixtures. And then there's 2.5s in overhead in running the tests via a test console (so that may not be a problem in CI).

Let's make the test class bigger.

class SomethingTest < ActiveSupport::TestCase
  test "something" do
  end

  test "something2" do
  end

  test "something3" do
  end
end

With no fixtures:

Tanda @ Rails 6.0.3.7 (test) :010 > Benchmark.ms { test('unit/something') }
Run options: --seed 35867

# Running:

-----------------------------
SomethingTest: test_something
-----------------------------
.------------------------------
SomethingTest: test_something2
------------------------------
.------------------------------
SomethingTest: test_something3
------------------------------
.

Finished in 0.020280s, 147.9322 runs/s, 0.0000 assertions/s.

3 runs, 0 assertions, 0 failures, 0 errors, 0 skips
2448.007049970329

With fixtures:

Tanda @ Rails 6.0.3.7 (test) :016 > Benchmark.ms { test('unit/something') }
Run options: --seed 9946

# Running:

-----------------------------
SomethingTest: test_something
-----------------------------
.------------------------------
SomethingTest: test_something2
------------------------------
.------------------------------
SomethingTest: test_something3
------------------------------
.

Finished in 1.044996s, 2.8708 runs/s, 0.0000 assertions/s.

3 runs, 0 assertions, 0 failures, 0 errors, 0 skips
3062.3492379672825

So it takes basically the same amount of time if you have 1 test or 3 tests. That sounds weird. Fixtures are loaded in before setup, then the test runs in a transaction after they are loaded. But the docs say that method runs before every test.

Let's check:

class SomethingTest < ActiveSupport::TestCase
  def before_setup
    puts 'hi'
    super
  end

  test "something" do
  end

  test "something2" do
  end

  test "something3" do
  end
end

Output:

Tanda @ Rails 6.0.3.7 (test) :018 > Benchmark.ms { test('unit/something') }
Run options: --seed 41967

# Running:

hi
-----------------------------
SomethingTest: test_something
-----------------------------
.hi
------------------------------
SomethingTest: test_something2
------------------------------
.hi
------------------------------
SomethingTest: test_something3
------------------------------
.

Finished in 1.010500s, 2.9688 runs/s, 0.0000 assertions/s.

3 runs, 0 assertions, 0 failures, 0 errors, 0 skips
3092.0356039423496

🤷‍♂️ I guess there's something in Rails that makes the second fixture load faster than the first. I wonder if this works across files? I made two files in a dir that are identical.

Tanda @ Rails 6.0.3.7 (test) :020 > Benchmark.ms { test('unit/something/**') }
Run options: --seed 45892

# Running:

-----------------------------
SomethingTest: test_something
-----------------------------
.------------------------------
SomethingTest: test_something2
------------------------------
.------------------------------
SomethingTest: test_something3
------------------------------
.----------------------------------
SomethingElseTest: test_something3
----------------------------------
.---------------------------------
SomethingElseTest: test_something
---------------------------------
.----------------------------------
SomethingElseTest: test_something2
----------------------------------
.

Finished in 1.055187s, 5.6862 runs/s, 0.0000 assertions/s.

6 runs, 0 assertions, 0 failures, 0 errors, 0 skips
3486.501968000084

Yep. Looks like fixtures are loaded once (ever?) and then there's no real overhead for each additional test. So that means that decreasing our fixture load will probably not make CI faster (since we only incur the 1s overhead once).

So the original hypothesis was wrong. That said, we could shave up to 1s off each test run in a test console by cutting our fixture usage. Without a corresponding CI gain that's not very exciting though - the simplicity of using fixtures is pretty nice.

@ghiculescu
Copy link
Author

Turns out we use self.pre_loaded_fixtures = true so this is probably all wrong.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment