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
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
Building a Rails Engine