Overcoming Inertia: Autogenerating Specs With ERb And Sequel

Momentum is an important factor when programming. Once you’ve been coding for a while momentum takes over and before you know it it’s 2:15 in the morning and you’ve got 248 tests passing without a hitch.

The enemy of momentum however is inertia,that initial hump you need to get over to get the code ball rolling. Many an hour has been wasted by a developer staring at an empty Vim window, trying to decide whether to write ‘class User’ or ‘class Member’ or ‘class Subscriber’, not to mention deciding what needs to be tested etc etc.

Sometimes you just need a kick in the coding pants to get things going and what better way to deliver the aforementioned kick than to use the built-in introspection of Sequel::Model to extract information about a models attributes and then our trusty friend ERb to autogenerate some RSpec testing code to get you started!

Note: Don’t forget to read this article to see how to implement improved command line arguments for Rake tasks.

First let’s look at the command that we run to kick everything off:

1 $ rake generate:model_auto_spec  model=User

We call a Rake task and pass through an environment variable containing the model we wish to generate our specs for. Let’s look at the Rake task:

 1 require 'erb'
 2 
 3 namespace :generate do
 4   desc 'Generates an rspec skeleton for a model'
 5   task :model_auto_rspec => 'sequel:merb_start' do
 6     model_class = ENV['model']
 7     model_instance_name = model_class.downcase
 8     model_file = "#{ Merb.config[ :merb_root ] }/app/models/#{ model_instance_name }"
 9     model_spec_file = "#{ Merb.config[ :merb_root ] }/spec/models/#{ model_instance_name }_auto_spec.rb"
10     model_instance = "@#{model_instance_name}"
11     template_file  = "#{ Merb.config[ :merb_root] }/lib/tasks/rspec_model_skeleton.erb"
12     model = Object.const_get( model_class )
13     File.open(template_file,"r") do |input|
14       File.open( model_spec_file, "w" ) do |output|
15         output.puts( ERB.new( input.readlines.join, nil, "-" ).result( binding ))
16         puts "Wrote auto generated rspecs to #{ model_spec_file }"
17       end
18     end
19   end
20 end

In lines 6-11 we set up our environment, constructing the names of the various files we’ll use, for instance the generated specs will be written to ‘spec/models/user_auto_spec.rb’. On line 12 we use Object.const_get to retrieve the actual class from the class name. Starting on line 13 we read in our template file, which for models is ‘rspec_model_skeleton.erb’ and use ERb to parse it and generate our finished file.

It’s a good idea to write the auto-generated specs to a seperate file than ‘spec/models/user_spec.rb’. That way we can regenerate the specs in ‘user_auto_spec.rb’ later on and not wipe out the handwritten specs in ‘user_spec.rb’.

Here’s part of the contents of a template file which generates some tests for a model that has been saved for the first time to the database:

 1 describe <%= model %>, "which has been initially saved" do
 2   before{ <%= model_instance %> = create_<%= model_instance_name %> }
 3   after{ clear_all_tables }
 4 
 5   it( "should be valid" ){ <%= model_instance %>.should be_valid }
 6   <%- model.db_schema.each do |column,column_info| -%>
 7   <%- unless column_info[ :allow_null ] -%>
 8   it( "should have a non null <%= column %>" ){ <%= model_instance %>.<%= column %>.should_not be_nil }  
 9   <%- end -%>
10   <%- end -%>
11 end

On line 6 we use Sequel::Model#db_schema to interrogate the database table and retrieve information about each column, which is returned in the column_info hash. We’re only interested in testing the attributes that are required to not be null and so on line 7 we we use the column_info to generate requirements only for attributes which have
column_info[ :allow_null ] == false

And after all is said and done the generated code looks like this:

 1 describe User, "which has been initially saved" do
 2   before{ @user = create_user }
 3   after{ clear_all_tables }
 4 
 5   it( "should be valid" ){ @user.should be_valid }
 6   it( "should have a non null password_hash" ){ @user.password_hash.should_not be_nil }  
 7   it( "should have a non null created_at" ){ @user.created_at.should_not be_nil }  
 8   it( "should have a non null updated_at" ){ @user.updated_at.should_not be_nil }  
 9   it( "should have a non null username" ){ @user.username.should_not be_nil }  
10   it( "should have a non null id" ){ @user.id.should_not be_nil }  
11   it( "should have a non null uuid" ){ @user.uuid.should_not be_nil }  
12 end

create_user and clear_all_tables are helper methods defined in our spec helper files.

As you test other models you’ll pull out common tests that apply across models and add them to your template file. Regenerating the auto spec files will then test your previously written models with the new tests as well.

So there you have it. No more needlessly fighting coding inertia, just a few keystrokes at the command line and you’re A for Away.


Farrel Lifson is a lead developer at Aimred.

About Aimred

Aimred is a specialist Ruby and Ruby on Rails development house and consultancy based in Cape Town, South Africa.

We provide Ruby and Ruby on Rails development, consulting and training services to businesses and organisations of all sizes. If you want to find out how we can help you, contact us at info@aimred.com.

Recent Posts

Yearly Archives