Cucumber plus ad-hoc apps to test Rails 3 generators

...because generators are code too!

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
provides a Rails::Generators::TestCase for you to easily test your generators. A good example extracted from Plataforma Tecnologia's Devise:

# test/generators/install_generator_test.rb
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 rspecforgenerators 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 rails generate 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:

# my_gem/lib/generators/my_gem/install_generator.rb
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.

# my_gem/Gemfile
gem 'aruba'  
# my_gem/features/support/env.rb
require 'aruba'  

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!

# my_gem/features/generator.feature
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 to create_posts.rb — the migration inserts a timestamp before those words.

Now you should have a well-tested Rails 3 generator and everything should be leaf-green!