Chris Wanstrath blogged yesterday about fixin' fixtures with fixture scenarios. He asked for good solutions, so I thought I would write up the solution we're using on my current ThoughtWorks project.
First, here are my top problems with fixtures:
(1) They're often test specific, yet available for every single test. Using the domain from my entry on Law of Demeter yesterday, a paper boy needs to be able to collect money from a customer. To test the edge cases, we need a customer who doesn't have enough money in his wallet. We could create fixtures for this scenario, but we are unlikely to reuse it much. I would rather see the data creation directly in the test than have to reference fixture files. This ties into my next point:
(2) Fixtures are not easy to maintain. This complaint is voiced the most. Chris made several good points around this in this blog post. I'm not going to go into detail on this, but fixtures are hard to maintain for several reasons. Often they end up incomplete, or missing relationships.
(3) Database state. Using fixtures as implemented in Rails leaves data laying around in the database after each testcase runs. Transactional tests are great, but unfortunately fixtures do not tear down. This means that even though a test case isn't using fixtures :model1, :model2, ..., it may still depend on certain data being in the database that gets left around after another test case loaded certain fixtures.
Enough ranting, onto the solution. We use a factory that has a method for each model named create_
module Factory def self.create_customer(attributes = {}) default_attributes = { :first_name => "Dan", :last_name => "Manges", :paperboy => create_paperboy } Customer.create! default_attributes.merge(attributes) end def self.create_newspaper(attributes = {}) default_attributes = { :customer => create_customer, :headline => "Read all about it!" :paperboy => create_paperboy } Newspaper.create! default_attributes.merge(attributes) end def self.create_paperboy(attributes = {}) default_attributes = { :first_name => "Paper", :last_name => "Boy", :delivery_route => "Main St Route" } Paperboy.create! default_attributes.merge(attributes) end end
This has worked really well for us. It differs slightly from Object Mother in that Object Mother has specific objects. The example from Martin's post is "Maybe 'John', an employee who just got hired last week; 'Heather' and employee who's been around for a decade." This is similar to having fixtures.
The factory provides default values and relationships. A record for any model should be able to be created using Factory.create_[model] without any arguments. The record will have default values and all associations.
Any record returned from the factory should be in a valid state. We use the create! method of ActiveRecord to ensure we cannot get an object in an invalid state.
Using the factory greatly improves the readability of our tests. This is how a test might look with using fixtures:
def test_paperboy_delivers_to assert_equal false, paperboys(:johnny).delivers_to?(customers(:george)) end
This test is difficult to understand. Most of the interesting details about the test are hidden away in yaml files. Here is the same test implemented using the factory:
def test_paperboy_delivers_to paperboy = Factory.create_paperboy customer = Factory.create_customer Factory.create_newspaper :paperboy => paperboy, :customer => customer assert_equal true, paperboy.delivers_to?(customer) end
Everything our test needs is now contained in the test method. This test is both easier to understand and maintain. A failing test will not lead to a rabbit hole down to fixture hell (which often leads to breaking other tests, since developers tend to share fixtures among tests).
If you leave the use_transactional_fixtures configuration option set to true (and you should), Rails will automatically rollback the queries your test ran at the end of the test. (This happens even without specifying fixtures at the top of the test, making the configuration option slightly mis-named). This prevents the problem of having different database state for different tests.
The downside to this approach is that creating a bunch of records for each individual test can be slow. On the project I'm on now, we have sped up our factory by applying default values (I'll blog about that modification later). Also, anytime we're testing using database records, we consider them functional model tests. Our unit tests around models do not hit the database at all and are very fast. This is a good approach when possible. However, sometimes tests need to use the database, and the factory is much better than fixtures for that.
Paul Gross uses the same approach with a different syntax.
Nathan Herald wrote a plugin, factories_and_workers, to streamline setting up a factory. For more information read his blog entry, the readme, or the source.
Scott Taylor wrote a plugin, FixtureReplacement, that also helps with defining a factory.