When developing Rails engines, implementing generators becomes a frequent use case. Thanks to the new approach to generators in Rails 3, based on Yehuda Katz's work on Thor, implementing flexible generators for your Rails engines not only is easier than before, but also offers powerful capabilities, such as hooks. Basically, hooks turn generators into composable pieces, the behavior of which you can adapt to suit your needs.
That sounds really useful — you might be thinking — but...
...how the heck do I test those?
A good generator usually messes with the filesystem in ways that we BDD cool kids are frightened about.
If you use
Test::Unit to test, you have nothing to fear — Rails
Rails::Generators::TestCase for you to easily test your generators.
A good example extracted from Plataforma Tecnologia's Devise:
class InstallGeneratorTest < Rails::Generators::TestCase tests Devise::Generators::InstallGenerator destination File.expand_path("../../tmp", __FILE__) setup :prepare_destination test "Assert all files are properly created" do run_generator assert_file "config/initializers/devise.rb" assert_file "config/locales/devise.en.yml" end end
Nevertheless, if you use RSpec, you have fewer options. Despite there are a couple of projects providing generator matchers for RSpec (Kristian Mandrup's rspec_for_generators and Colin MacKenzie IV's genspec), they are not fully compatible with latest Rails 3/RSpec 2 stack, or they require many dependencies some of which (in my experience) just don't work with Ruby 1.9.2.
Cucumber to the rescue!
A pretty good way to test our generators is at integration level. In the end,
our final engine user will fire up the command line and type
our_plugin:install or something like that, so theoretically we could use
Cucumber for that! In practice, indeed, there is a useful
tool to deal with command-line steps from Cucumber — Aruba.
Combining both tools we have all we need. Let's see it in a practical example. Suppose we have a generator like this:
require 'rails' require 'rails/generators' module MyGem module Generators class InstallGenerator < Rails::Generators::Base desc "Install MyGem, with its posts' migration and routes" source_root File.expand_path("../templates", __FILE__) def add_initializer template "my_initializer.rb", "config/initializers/my_initializer.rb" end def add_routes route "resource :posts" end def copy_migration migration_template 'migration.rb', 'db/migrate/create_posts.rb' end end end end
We have to include Aruba in both our Gemfile and Cucumber environment, so that we can call its steps from Cucumber scenarios.
Now for the Cucumber scenario. We will call it
generator.feature, and it will
basically generate a new Rails application from scratch with a Gemfile pointing
to our engine. Aruba saves this new application under a
tmp/aruba folder (which you may want to add to your
.gitignore). Then we will call our generator
from this new app and see what happens!
Feature: In order to save the world As an open-source evangelist I want everyone to use MyGem and its generators. Scenario: my_gem:install generator bootstraps the basic files Given I run "rails new my_app" And I cd to "my_app" And a file named "Gemfile" with: """ source "http://rubygems.org" gem 'rails' gem 'sqlite3' gem 'my_gem', :path => '../../../' """ When I run "bundle install" And I run "rails generate my_gem:install" Then the following files should exist: | config/initializers/my_gem.rb | And the file "config/routes.rb" should contain "resource :posts" When I run "ls db/migrate/" Then the output should contain "create_posts.rb"
As you can see, since
aruba doesn't support pattern matching on filenames yet,
we have to make Cucumber run an
ls db/migrate/ to ensure there is a filename similar
create_posts.rb — the migration inserts a timestamp before
Now you should have a well-tested Rails 3 generator and everything should be leaf-green!