Recently I gave a talk at RubyKaigi 2014 about Rails fixtures. In this blog post I will be discussing some of the tips and tricks for using fixtures effectively.

You can also the see the video of this talk here.

Don’t use ids.. unless required

In fixtures, we can specify id of a fixture.

john:
  id: 1
  email: john@example.com

I would recommend not to specify the id. Rails generates the id automatically if we don’t explicitly specify it. Moreover there are a few more advantages of not specifying the id.

Stable ids for every fixture.

Rails will generate the id based on key name. It will ensure that the id is unique for every fixture. It can also generate ids for uuid primary keys.

Labeled references for associations like belongs_to, has_many.

Lets say we have a users table. And a user has many cars.

Car ferrari belongs to john. So we have mentioned user_id as 1.

ferrari:
  name: ferrari
  make: 2014
  user_id: 1

When I’m looking at cars.yml I see user_id as 1. But now I to lookup to see which user has id as 1.

Here is another implementation.

john:
  name: john
  email: john@example.com

ferrari:
  name: ferrari
  make: 2014
  user: john

Notice that I no longer specify user_id for John. I have mentioned name. And now I can reference that name in cars.yml to mention that ferrari belongs to john.

How to set a value to nil from fixture

Let’s say that I have a boolean column which is false by default. But for an edge case, I want it to be nil. I can obviously mutate the data generated by fixture before testing. However I can achieve this in fixtures also.

Specify null to make the value nil

require 'yaml'
YAML.load "--- \n:private: null\n")
=> {:private=>nil}

As you can see above if the value is null then YAML will treat it as nil.

john:
  name: john
  email: john@example.com
  private: null

Leave the value blank to make the value nil

require 'yaml'
YAML.load "--- \n:private: \n")
=> {:private=>nil}

As you can see above if the value is blank then YAML will treat it as nil.

john:
  name: john
  email: john@example.com
  private:

When model name and table name does not match

Generally in Rails, the model name and table name follow a strict convention. Post model will have table name posts. Using this convention, the fixture file for Post models is obviously fixtures/posts.yml.

But sometimes models do not match directly with the table name. This could be because of legacy reason or because of namespacing of models. In such cases automatic detection of fixture files becomes difficult.

Rails provides set_fixture_class method for this purpose. This is a class method which accepts a hash where key should be name of the fixture or relative path to fixture file and value should be model class.

I can use this method inside test_helper.rb in any class inheriting from ActiveSupport::TestCase.

# test_helper.rb
class ActiveSupport::TestCase

  # table name is "morning_appts". It is being mapped to model "MorningAppointment".
  self.set_fixture_class morning_appts: MorningAppointment

  # in this case fixture is namespaced
  self.set_fixture_class '/legacy/users' => User

  # in this case the model is namespaced.
  self.set_fixture_class outdoor_games: Legacy::OutdoorGame
end

values interpolation using $LABEL

Rails provides many ways to keep our fixtures DRY. Label interpolation is one of them. It allows the use of key of fixture as a value in the fixture. For example:

john:
  name: john
  email: john@example.com

becomes:

john:
  name: $LABEL
  email: john@example.com

$LABEL is not a global variable here. Its just a placeholder. $LABEL is replaced by the key of the fixture. And as discussed earlier the key of the fixture in this case is john. So $LABLE has value john.

Before this PR, I could only use this feature if the value is exactly $LABEL. So if the email is john@example.com I could not use the $LABEL@example.com. But after this PR, I can $LABEL anywhere in the string, and Rails will replace it with the key.

So the earlier example becomes:

john:
  name: $LABEL
  email: $LABEL@example.com

YAML defaults

I use YAML defaults in database.yml for drying it up and keeping common configuration at one place.

defaults: &defaults
  adapter: postgresql
  encoding: utf8
  pool: 5
  host: localhost
  password:

development:
  <<: *defaults
  database: wheel_development

test:
  <<: *defaults
  database: wheel_test

production:
  <<: *defaults
  database: wheel_production

I can use it for drying up fixtures too for extracting common part in our fixtures.

DEFAULTS: &DEFAULTS
  company: BigBinary
  website: bigbinary.com
  blog: blog.bigbinary.com

john:
  <<: *DEFAULTS
  name: John Smith
  email: john@bigbinary.com

prathamesh:
  <<: *DEFAULTS
  name: Prathamesh Sonpatki
  email: prathamesh@bigbinary.com

Note the usage of key DEFAULTS for defining default fixture. Rails will automatically ignore any fixture with key DEFAULTS.

If we use any other key then a record with that key will also get inserted in the database.

Database specific tricks

Fixtures bypass the normal Active Record object creation process. After reading them from YAML file, they are inserted into database directly using insert query. So they skip callbacks and validations check. This also has an interesting side-effect which can be used for drying up fixtures.

Suppose we have fixture with timestamp:

john:
  name: John Smith
  email: john@example.com
  last_active_at: <%= Time.now %>

If I are using PostgreSQL, I can replace the last_active_at value with now:

john:
  name: John Smith
  email: john@example.com
  last_active_at: now

now is not a keyword here. It is just a string. The actual query looks like this:

INSERT INTO "users"
("name", "email", "last_active_at", "id")
VALUES
('John Smith', 'john@example.com', 'now',1144934)

So the value for last_active_at is still just now when the query is executed.

The magic starts as PostgreSQL starts reading the values. now is a shorthand for the current timestamp . As soon as Postgres reads it, it replaces now with the current timestamp and the column last_active_at gets populated with current timestamp.

I can also use the now() function instead of just now.

This function is available in PostgreSQL as well as MySQL. So the usage of now() works in both of these databases.