[rspec-users] Rails' fixtures suck! But what about something like this?

Carl Lerche carl.lerche at gmail.com
Mon May 21 16:36:53 EDT 2007


Sorry about the very long email, but this is a hairy topic that's been
annoying me for some time and I decided to try to do something about.
Also, if you got this twice, I apologize too, but it didn't seem to
have successfully gone out the first time.

Background:
----------

I've been dealing with Rails for about a year and a half now. I've
been using Rails' built in testing framework, and it's fine... until I
really have to start dealing with the fixtures. I am currently working
on an application that really focuses on selecting data using complex
logic. The only way to test the application in a meaningful way is by
using a large number of fixtures. This has become a real nightmare to
manage and I almost am spending more time keeping the fixtures up to
date than I am coding or testing. So, I took a moment, stepped back,
and took a good look at how I could I could improve fixtures to make
my life a lot easier.

I have gotten a quick and dirty prototype of my solution running, but
before I really roll up my sleeves and start polishing it up, I
thought I would try to get some suggestions from the community.

Overview:
---------

In my experience, the current fixture framework has three main problems:

- There is no way to create different version of fixtures. What I mean
by this is that you only can have one users.yml file even if your
tests depend on different initial conditions. What this usually means
is that you will just keep tagging on to the end of users.yml even
though it is not the most efficient way to do it.
- Managing relationships between tables is a really big pain. One must
manually keep track of IDs. This can quickly become a major headache.
- There is no clean way to quickly generate data. If  I want 50
records that I could easily generate in a loop, I still gotta write
them by hand (at least, I wasn't able to figure out how to add loops
to my yaml files in a clean way).
- There is a lot of repetition. No way to follow DRY.

Solution:
---------

+ Versioned Fixtures

First, to address the fixture "versions", I thought the easiest way to
do this would be scenarios (I was inspired from fixture-scenarios).
The way fixtures could be laid out is as follows:

+ specs
|- fixtures
   |- global
   |- only_one_signed_up_user
   |- lots_of_posts_in_los_angeles
   |- ...

All global fixtures (fixtures that should be loaded before every spec)
go into global, then, for each different scenario, there is a
directory created with the relevant fixtures in there. Then, in the
specs, you could do the following

-------------------------------
require File.dirname(__FILE__) + '/../spec_helper'

describe User, :when => :only_one_signed_up_user do

  it "should be the only user" do
    User.count.should == 1
  end
end
-------------------------------

The key part here would be the :when => :scenario_name option to the
describe method.

+ Writing the fixtures

Writing fixtures with YAML is quite ugly. I think a better way to do
it would be using a DSL, but coming up with a good syntax is hard.
This is where I am the most hesitant. I have two "options" and I was
wondering if people could comment on them and give me some feedback
and suggestions.

- The first option is how I started out: http://pastie.caboo.se/63359

In this option, fixtures for multiple tables can be written in a
single file. You would start out by using the _create_ method to tell
the parser what table the fixtures are for and you could also specify
:with, which would set default attributes for fixtures.

You could also nest fixtures in order to easily scope them (see the
last user fixture how I specified nested post fixtures).

- The second option is what I currently prefer: http://pastie.caboo.se/63361

In this option, I keep the one table per file concept that is
currently used. As such, we don't need to specify what table we want
to use before hand. I make available a with_options method which lets
you set default attributes for fixtures. It's also easy to create
loops to automatically generate fixtures, as I show in the second half
of the file. The f method is the actual method for creating a fixture.
it takes a name as a string and a block in which the attributes of the
fixtures are specified. However, the method_missing method is
implemented to handle any missing methods and use that to create
fixtures too as seen with fixture_name { ... }

+ Dealing with relationships

I think what I hate the most about fixtures right now is having to
deal with table relationships. Having to keep track of IDs myself is
horrible. So, I thought that the easiest way to do this is making the
table_name(:fixture_name) method available straight inside the
fixtures. So, if I have a user model that has a location associated
with it (let's say a zip code, longitude, and latitude), instead of
having to copy / paste that data across fixtures, I can do the
following:

-------------------------------
local_guy {
  zip_id	zips(:portland)
  longitude	zips(:portland).longitude
  latitude	zips(:portland).latitude
}
-------------------------------

This is much easier to deal with than the other way around. This also
introduces dependencies between fixtures, I thought the easiest way to
handle it would be adding a fixture_order configuration:

-------------------------------
Spec::Runner.configure do |config|
  config.fixture_order = :zips, :categories, :users, :posts
end
-------------------------------

+ What's not solved with this

I haven't figured out a good way to handle counter_cache columns.
Right now, this would still require you to manually count how many
fixtures you have associated with a parent and add that to the
counter_cache column. Any ideas of a better way to do this?

Well, that's all I came up with. I'm going to keep hacking away at
this and hopefully you all have some feedback to improve this concept.

-- 
EPA Rating: 3000 Lines of Code / Gallon (of coffee)


More information about the rspec-users mailing list