September 21, 2014
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
In fixtures, we can specify id of a fixture.
john:
id: 1
email: [email protected]
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.
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.
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: [email protected]
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
.
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.
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: [email protected]
private: null
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: [email protected]
private:
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
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: [email protected]
becomes:
john:
name: $LABEL
email: [email protected]
$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
[email protected]
I could not use the [email protected]
.
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: [email protected]
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: [email protected]
prathamesh:
<<: *DEFAULTS
name: Prathamesh Sonpatki
email: [email protected]
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.
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: [email protected]
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: [email protected]
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', '[email protected]', '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.
If this blog was helpful, check out our full blog archive.