I maintain a ruby gem that was without a database migration generator for far too long. This is a walkthrough on how I TDD‘d a generator to create its database migration generator.
Our end goal is to create the database migration install action for rails generate double_double:install
What’s a migration generator?
A migration generator is the composition of ActiveRecord migrations within Rails generators. They are commonly found in Rails engines and plugins.
Rails generators are convenience scripts. Most frequently generators are used to create or insert boilerplate code into an application.
# An example Rails generator that will
# generate a new UsersController
#
$ rails generate controller User
ActiveRecord migrations are Ruby classes that define a version of the application’s database. Where each additional database migration only defines how to incrementally modify the database from the previous version.
# Example database migrations to add a column to an existing model
#
$ rails generate migration add_email_to_users email:string
Now if we were to combine both of these concepts we arrive at migration generators. Which are generators that are capable of creating database migrations.
# Devise will create a database migration for the Users table
#
$ rails generate devise User
# Creates a database migration to support my
# double-entry accounting gem
#
$ rails generate double_double:install
TDD approach
For the purpose of this generator, I am most concerned that the database migration template is copied into the expected location.
We could further inspect the generated migration for its specific content, but that’s approaching overkill. As other tests utilize the database, those tests will certainly fail loudly if there is a problem with our migration.
To assist in our tests we will be using the generator_spec gem. Let’s
add it to our .gemspec
. Bundle install.
# double_double.gemspec
Gem::Specification.new do |gem|
# ...
gem.add_development_dependency 'generator_spec'
end
Our test
The skeleton of our Rspec spec:
# spec/lib/generators/double_double/install/install_generator_spec.rb
require "generator_spec"
module DoubleDouble
module Generators
describe InstallGenerator, :type => :generator do
root_dir = File.expand_path("../../../../../../tmp", __FILE__)
destination root_dir
before :all do
prepare_destination
run_generator
end
end
end
end
Our most significant decision so far was deciding where to temporarily store
the generated files and directories. The tmp
directory is an obvious choice.
Let’s continue and add our test case.
# spec/lib/generators/double_double/install/install_generator_spec.rb
require "generator_spec"
module DoubleDouble
module Generators
describe InstallGenerator, :type => :generator do
root_dir = File.expand_path("../../../../../../tmp", __FILE__)
destination root_dir
before :all do
prepare_destination
run_generator
end
it "creates the installation db migration" do
migration_file =
Dir.glob("#{root_dir}/db/migrate/*create_double_double.rb")
assert_file migration_file[0],
/class CreateDoubleDouble < ActiveRecord::Migration/
end
end
end
end
Now we have a test to code against. This test will assert:
- That the migration exists
- That some text in the migration is the classname that we expect
Production code
Fast-forward some TDD and a few cups of coffee…
We wrote a proper database migration template.
# lib/generators/double_double/install/templates/create_double_double.rb
class CreateDoubleDouble < ActiveRecord::Migration
def change
# ...
end
end
And to finish the task we added the actual Rails generator.
# lib/generators/double_double/install/install_generator.rb
require 'rails/generators'
require 'rails/generators/migration'
module DoubleDouble
module Generators
class InstallGenerator < ::Rails::Generators::Base
include Rails::Generators::Migration
source_root File.expand_path('../templates', __FILE__)
desc "Add the migrations for DoubleDouble"
def self.next_migration_number(path)
next_migration_number = current_migration_number(path) + 1
ActiveRecord::Migration.next_migration_number(next_migration_number)
end
def copy_migrations
migration_template "create_double_double.rb",
"db/migrate/create_double_double.rb"
end
end
end
end
You may be asking: What’s the class method next_migration_number
?
It determines the date and time string that will be prepended on the migration filename. I found that exact method implementation in Rails, itself.