Not long ago Pete Yandell bestowed upon us an alternative to FactoryGirl and other fixture methods: Machinist.
Unfortunately I hadn’t had a chance to check it out before my Cucumbers and Factory Girls presentation last month at RORO Sydney so I couldn’t say much about it, but I’ve just started using for the 2009 revision of the LGSA Cultural Awards website.
What’s Machinist?
Equipped with a Lathe, Machinist allows you to craft beautifully valid and well-populated ActiveRecord models.
It’s extremely handy if you want an alternative to YML fixtures or you’re sick seeing the ugly save(false).
Machinist also includes Sham: a class allowing to you to create repeatable dummy data for your text fixtures. Sham helps to remove magic strings whilst ensuring your test data is deterministic.
Installing Machinist
It’s a Rails plugin, so simply:
./script/plugin install git://github.com/notahat/machinist.git
Then create your spec/blueprint.rb and require it from your spec/spec_helper.rb like so:
require File.join(File.dirname(__FILE__), 'blueprint')
Similarly for your features/steps/env.rb:
require File.join(RAILS_ROOT, 'spec', 'blueprint')
Defining your blueprints
Machinist extends ActiveRecord::Base with the class methods blueprint, for defining the blueprint:
Post.blueprint do
title "Hello world"
text "<p>Hello there</p>"*3
slug "some-slug"
published_on Date.today
end
and make, for creating objects from the blueprint:
post = Post.make
post.new_record? #=> false
post.title #=> "Hello world"
Sham on you!
Machinist includes Sham: a class allowing to you to create repeatable dummy data for your text fixtures.
You define your sham’s like so (here we’re using the Faker gem):
Sham.title { Faker::Lorem.sentence }
Sham.body { Faker::Lorem.paragraphs }
Sham.slug { Faker::Lorem.words(1).first.downcase }
Note: shamelessness is also obtainable through the addition of a single letter:
uninitialized constant Shame (NameError)
Using your shams in your blueprint is easy:
Post.blueprint do
title { Sham.title }
text { Sham.body }
slug { Sham.slug }
published_on Date.today
end
You don’t have to use Faker in your sham—to generate a random date you could simply do:
Sham.date do
Date.civil((1990...2009).to_a.rand,
(1..12).to_a.rand,
(1..28).to_a.rand)
end
and use it in your blueprint just like the others:
Post.blueprint do
title { Sham.title }
text { Sham.body }
slug { Sham.slug }
published_on { Sham.date }
end
Using them with specs and features
To use your blueprints in your specs simply call make:
describe "Blog#to_param" do
it "returns 'year-slug'" do
blog = Blog.make
blog.to_param.should == "#{blog.year}-#{blog.slug}"
end
end
It’s exactly the same for features/stories:
Given "there's a blog post" do
Post.make
end
How does it compare to FactoryGirl?
Machinist’s syntax is definitely cleaner. The equivalent blueprint in FactoryGirl looks like:
Factory.define :post do |p|
u.title "Hello world"
p.text "<p>Hello there</p>"*3
p.slug "some-slug"
p.published_on Date.today
end
or more realistically:
Factory.define :post do |p|
p.title "Hello world"
p.text "<p>Hello there</p>"*3
p.slug { Factory.next(:slug) }
p.published_on { Factory.next(:published_on_date) }
end
Factory.sequence(:slug) {|n| "slug-#{n}"}
# ...
To generate sequences in sham you simply accept a block parameter, for example:
Sham.slug {|n| "slug-#{n}" }
Machinist’s ways of defining associations are also much cleaner if everything matches up:
Comment.blueprint do
post
end
and if you’ve an association with a custom class_name you can call make yourself:
Post.blueprint do
author { User.make }
end
Contribute or follow development
The machinist github repo is the best place to start.
You could even move webjam’s spec/factories.rb to Machinist and send a pull request if you’re really keen ;)
If you’re a Victorian head to the October Melbourne RORO meetup where Pete will be demonstrating how to lathe Ruby objects with his new contraption.
Archived comments
Comments were previously allowed on articles. Though no new comments are being accepted you can see the old comments below.
-
Thanks for the write-up! A few things to add that haven’t made it into the documentation yet:
A blueprint should take care of creating all the data that your test doesn’t care about, leaving you to define the data that it does care about in the test setup. For example, you might have an editable flag on a comment and want to write some tests for it. In your test you want to be able to write:
@comment = Comment.make(:editable => true)
But a comment can’t exist without an author, and some body text, and a post. So you put those in the blueprint:
Comment.blueprint do post { Post.make } author { Sham.author_name } body { Sham.comment_body } endYou can even abbreviate this to:
Comment.blueprint do post author { Sham.author_name } body { Sham.comment_body } endMachinist will automatically do the Post.make for you.
You mentioned that Machinist doesn’t have sequence generation. Wrong! Sham lets you do this:
Sham.author_name {|index| "Author number #{index}" }There will be some more extensive docs (and probably a screencast) on all of this sometime soon.
-
thanks Pete.
That’s rockin, I didn’t know about the sequence generation.
I’ve updated the Machinist vs FactoryGirl section with new details and examples.
-
We’re using machinist + faker. They make me happy.
-
Hi all, and thanks for the informative post, Tim!
I’ve been using Faker’s Lorem Ipsum generator to generate seed data in my apps. I got a bit tired of looking at the unrealistic Latin through the app, so I ended up using markov chains & real english source data to generate more realistic looking seed data. This might be worth a look if you’re using Faker for more than just populating models in your tests.
I wrote up the approach here: http://log.openmonkey.com/post/55783578/using-markov-chains-to-provide-english-language-seed
-
Sorry, here is the link formatted to be click-friendly :)
Using Markov Chains to provide English language seed data for your Rails application
-
Your equivalent FactoryGirl samples all call methods on p, except for the first line (“u.title [...]”). Typo?
-
@drnic happy is good. Good is happy.
@timriley nice… interesting about Markov. For sample data that’d be mighty handy.
@sam: yep, a typo. Fixed and thanks!
-
Any idea how to use this from IRB? I think it would be convenient for testing out different scenarios in irb as well.
-
@johnwright: yeah you should be able to simply
require 'spec/blueprint'and off you go. -
Yes, that works assuming your RAILS_ENV=test so that machinists and sham are required and the machinist module is included in ActiveRecord::Base as is done in vendor/plugins/machinists/init.rb.