Test_Fixtures_in_Ruby_on_Rails_do_not_scale

Quick Profile

Alex Ooi Profile Picture
Hometown: Melbourne, Australia
Specializations: Java, Ruby on Rails
University: Software Engineering & Economics, Melbourne University
High School: VCE, Melbourne High School
Links:
 

Popular Articles

Tuesday, June 17, 2008

I believe Test Fixtures in Ruby on Rails are, for the most part, evil.

Test Fixtures are nice in theory

They are, of course, the ubiquitous *.yml files that reside in either your spec/fixtures or test/fixtures directories. Whilst they are great for getting some test data into your database really quick, and ensuring that your test database is in a known state before each test is run, there are some very serious flaws with them. Particularly when your project team increases to more than just 1 person and/or when your application's database schema begins to grow in size.

Flaws

Test Fixtures create shared test data

This is the KEY reason to stay away from Test Fixtures. Use Test Fixtures for what it is intended for (adding data to be asserted and checked for in tests) will quickly result in very brittle tests. The simple reason being that the test data is shared across all tests that include that fixture. Adding, updating or deleting any entries in a yml file will inevitably break tests that depend on certain data. It is not hard to imagine breaking hundreds of tests as a result of a few simple changes to a test fixture file, especially when working with any non-trivial Rails project.

There are some techniques that I have seen to try to workaround this limitation. However, they simply do not address all possible scenarios that need to be tested. These techniques are also inevitably more verbose and involve including much more logic in test code. This is bad. There are many scenarios where, for simplicity and practicality, you will simply want to rely on the overall state of your tables. Theres no two ways about it. Test Fixtures makes your test suite brittle and workarounds make your test suite more complex.

Test Fixtures are notoriously hard to setup non-trivial data

Try using Test Fixture files to set up associations when there are many join tables involved. Any non-trivial application will have its fair share of one-to-many and many-to-many relationships. Join tables in particular are notoriously hard to setup, often requiring 3 yml files to be open and referenced at the same time (2 for each of the model table ymls involved, and 1 for the join table yml).

In contrast, saving objects through ActiveRecord's persistence framework gives the developer a very OO way to create test data. Most importantly it takes care of any join tables. It just makes so much more sense to save data like so:

Product.create!(:brands => [brand_one, brand_two], 
                :retailers => [retailer_one, retailer_two])

rather than through the cumbersome yml files.

Forgetting to declare a Test Fixture leaves the database in an unknown state

The Fixtures that a test depends on are typically declared at the beginning of a test, such as:

fixtures :products, :retailers

This ensures that the appropriate YML files are run, and the data in those tables are in a known state. However, forgetting to include a test fixture for a table that the test depends on (in this case the join table between products and retailers, conventionally named products_retailers) will result in the join table being in an unknown state. The above line should really as such:

fixtures :products, :retailers, :products_retailers

This means that whilst the test may pass because the join table happens to be in a particular state, it will probably fail on someone else's box. This is very dodgy.

A naive solution would be to always run all fixtures in the 'fixtures' directory before each test. However, this is crap when running controller/view/helper tests, as these tests should really not be touching the database. Instead, a more appropriate solution is to create a method 'use_fixtures' which is accessible from any test run and which, when called, will run through all fixtures in the 'fixtures' directory and execute them to setup the test database. This allows model/integration tests to use fixtures simply by declaring 'use_fixtures' as such:

describe Product do

  use_fixtures

  it 'should do something' do
    ...
  end
end

Thus, any tests which are run against a database will have all fixture files executed, and any tests which are not run against the database simply do not need to include the 'use_fixtures' line. A good place to put the 'use_fixtures' method is in the spec_helper.rb file (when using RSpec).

Solution

I believe Test Fixtures have a role to play in test suites, but should be used selectively. Following are some of the ways in which I would use Test Fixtures:

Use Test Fixtures ONLY for data that is absolutely common to the entire test suite

Such an example is saving an administrator user, perhaps with an administrator role. This sort of data will always be useful for test runs, and it is quite ok for all tests to depend on a user with the login 'admin' and password 'password' to be present in the system.

However, in the large majority of cases, I would leave test fixtures empty and use the test fixtures framework simply to ensure that the database tables are in a known state (mostly empty, except for the odd exception such as administrator user).

Create data in setup methods

This is my preferred way to create data. Sure, it may involve a bit more repitition (need to write the code to save new objects each time) but thats what the before(:each) method is there for! The huge advantage is that your test code is simplified because it knows the current state of the database and can thus create explicit assertions. Also, test code is no longer brittle. Changing test data setup in the before(:each) method will only affect all tests run within the scope of that before(:each) and not other tests.

It maybe slightly more repetitive, but I believe that the advantage (no brittle tests and OO way of saving data) far outweighs the repetition involved, especially when your team or code base increases.

Implement a builder pattern for complex data relationships

Complex relationships with many associations, such as in the case of products, can be quite verbose to setup in code (have to save all associations as well as the product itself). Thats where a builder pattern, such as described here:

http://geekswithblogs.net/Podwysocki/archive/2008/01/08/118362.aspx

This allows you to quickly create a default product, whilst giving the flexibility of overwriting attributes to suit your particular test.

Comments ...

Man, you've changed.
Posted by Danny on Sunday, June 22, 2008 at 08:48 AM
I hear ya man. This is what we are doing now
Posted by Moh on Monday, July 14, 2008 at 02:47 PM
Well written article, thanks. Had heard the "fixtures is evil" meme,
but I hadn't seen someone explain as clearly "why", before coming
here.
Posted by Lars Westergren on Thursday, March 05, 2009 at 08:26 PM

Add a Comment

*
*
You must answer the following simple maths question before your comment will be accepted.
*