Rails Testing Antipatterns: Fixtures and Factories
In the upcoming series of posts, we’ll explore some common antipatterns in writing tests for Rails applications. The presented opinions come from our experience in building web applications with Rails (we’ve been doing it since 2007) and is biased towards using RSpec and Cucumber. Developers working with other technologies will probably benefit from reading as well.
Antipattern zero: no tests at all
If your app has at least some tests, congratulations: you’re among the better developers out there. If you think that writing tests is hard — it is, but you just need a little more practice. I recommend reading the RSpec book if you haven’t yet. If you don’t know how to add more tests to a large system you inherited, I recommend going through Working Effectively with Legacy Code. If you have no one else to talk to about testing in your company, there are many great people to meet at events such as CITCON.
If you recognize some of the practices discussed here in your own code, don’t worry. The methodology is evolving and many of us have “been there and done that”. And finally, this is all just advice. If you disagree, feel free to share your thoughts in the comment section below. Now, onwards with the code.
Fixtures and factories
Fixtures are Rails’ default way to prepare and reuse test data. Do not use fixtures.
Let’s take a look at a simple fixture:
# users.yml marko: first_name: Marko last_name: Anastasov phone: 555-123-6788
You can use it in a test like this:
describe User do describe "#full_name" do it "is composed of first and last name" do user = users(:marko) user.full_name.should eql("Marko Anastasov") end end end
There are a few problems with this test code:
- It is not clear where the user came from and how it is set up.
- We are testing against a “magic value” — implying something was defined in some code, somewhere else.
In practice these shortcomings are addressed by comment essays:
describe Dashboard do fixtures :all describe "#show" do before do # User with preferences to view posts about kittens # and in the group with special access to Burmese cats # with 4 friends that like ridgeback dogs. @user = users(:kitten_fan) end end end
Maintaining fixtures of more complex records can be tedious. I recall working on an app where there was a record with dozens of attributes. Whenever a column would be added or changed in the schema, all fixtures needed to be changed by hand. Of course I only recalled this after a few test failures.
A common solution is to use factories. If you recall from the common design patterns, factories are responsible for creating whatever you need to create, in this case records. Factory Girl is a good choice.
Factories let you maintain simple definitions in a single place, but manage all data related to the current test in the test itself when you need to. For example:
FactoryGirl.define do factory :user do first_name "Marko" last_name "Anastasov" phone "555-123-6788" end end
Now your test can set the related attributes before checking for the expected outcome:
describe User do describe "#full_name" do before do @user = build(:user, :first_name => "Johnny", :last_name => "Bravo") end it "is composed of first and last name" do @user.full_name.should eql("Johnny Bravo") end end end
A good factory library will let you not just create records, but easily generate unsaved model instances, stubbed models, attribute hashes, define types of records and more — all from a single definition source. Factory Girl’s getting started guide has more examples.
Factories pulling too many dependencies
Factories let you specify associations, which get automatically created. For example, this is how we say that creating a new Comment should automatically create a Post that it belongs to:
FactoryGirl.define do factory :comment do post body "groundbreaking insight" end end
Ask yourself if creating or instantiating that post in every call to the Comment factory is really necessary. It might be if your tests require a record that was saved in the database, and you have a validation on
Comment#post_id. But that may not be the case with all associations.
In a large system, calling one factory may silently create many associated records, which accumulates to make the whole test suite slow (more on that later). As a guideline, always try to create the smallest amount of data needed to make the test work.
Factories that contain unnecessary data
A spec is effectively a specification of behavior. That is how we look at it when we open one. Similarly, we look at factories as definitions of data necessary for a model to function.
In the first factory example above, including
phone in User factory was not necessary, if there is not a validation of presence. If the data is not critical, just remove it.
Factories depending on database records
Adding a hard dependency on specific database records in factory definitions leads to build failures in CI environment. Consider the following example:
factory :active_schedule do start_date Date.today - 1.month end_date 1.month.since(Date.today) processing_status 'processed' schedule_duration ScheduleDuration.find_by_name('Custom') end
It is important to know that the code for factories is executed when the Rails test environment loads. This may not be a problem locally because the test database had been created and some kind of seed structure applied some time in the past. In the CI environment however the builds starts from a blank database, so the first Rake task it runs will fail. To reproduce and identify such issue locally, you can do
db:drop, followed by
One way to fix this is to use factory_girl’s traits:
factory :schedule_duration do name "Test Duration" trait :custom do name "Custom" end end factory :active_schedule do association :schedule_duration, :custom end
Another is to defer the initialization in a callback. This however adds an implicit requirement that test code defines the associated record before the parent:
factory :active_schedule do before(:create) do |schedule| schedule.schedule_duration = ScheduleDuration.find_by_name('Custom') end end
Got anything to add? I’d love to hear your comments below.
Update 2014-01-22: Changed fixtures examples to use key-based lookup.