Testing with fixtures best practices in rails


Tackling the Fixture Testing in Ruby on Rails

Brief insights into replacing traditional fixture-based testing with more dynamic approaches in Rails.

Issues

Brittleness in Tests

One of the biggest issues with fixtures is their brittleness. Minor adjustments can cascade into numerous failures across your test suite:

# file: test/models/post_test.rb
def test_changes_to_post_traits
  original_posts_count = Post.count
  posts(:example).update(title: "New Title")  # This may unintentionally break unrelated tests
  assert_not_equal original_posts_count, Post.count
end

Fixture Overload

As applications scale up, the addition of fixtures can become unwieldy:

# file: test/fixtures/posts.yml
100.times do |n|
  eval("post_#{n}:
       title: 'Post #{n}',
       body: 'Content of post #{n}',
       published: #{n.even?}")
end

Maintenance of this growing collection of fixtures can become a time-consuming task.

We all seen the 100’s of fixtures with weird names and we end up using our flow state to capture differences in a yml file…

Contextual Clarity

Fixtures, residing in a different file, offer no immediate context:

# file: test/models/user_test.rb
def test_user_capabilities
  user = users(:admin)  # 'admin' does not tell us from where it comes from
  assert user.can_manage?(resources(:server))
end

Data Integrity

Skipping ActiveRecord validations, fixtures may harbour invalid data states:

# file: test/fixtures/users.yml
john_doe:
  email: 'not_an_email'
  username: 'john_doe'

Here, email format validation is bypassed, potentially masking bugs.

Lifecycle Management

Skipping lifecycle hooks can lead to overlooking business logic encapsulated within them:

# file: app/models/user.rb
before_save :ensure_username_has_no_spaces

def ensure_username_has_no_spaces
  self.username = username.gsub(" ", "")
end

# file: test/fixtures/users.yml
jane_doe:
  username: 'jane doe'  # `ensure_username_has_no_spaces` is not triggered

Solutions

The Factory Alternative

Consider shifting to factory-based methods for test object creation. This approach leverages the Factory pattern, ensuring data validity and respecting lifecycle callbacks:

# file: test/factories/user_factory.rb
FactoryBot.define do
  factory :user do
    username { "john_doe" }
    email { "[email protected]" }

    trait :with_spaces_in_username do
      username { "john doe" }
    end
  end
end

# file: test/models/user_test.rb
class UserTest < ActiveSupport::TestCase
  test 'username is saved correctly' do
    user = FactoryBot.create(:user, :with_spaces_in_username)
    assert_equal "johndoe", user.username
  end
end

Here is the gem link

Embracing Contexts

Using contexts can improve setup redundancy and enhance test clarity:

# file: test/models/post_test.rb
context "Given a user" do
  setup { @user = FactoryBot.create(:user) }

  context "when the user is an admin" do
    setup { @user.admin = true }

    should "edit any post" do
      post = FactoryBot.create(:post)
      assert @user.can_edit?(post)
    end
  end
end

Replacing fixtures with factories and contexts not only addresses these core issues but also enhances maintainability and understandability of tests.


Related posts

Free Training By Email:
Building a Rails Engine

We will send udpates but we care about your data.
No spams.

Back to homepage