rSpec on merb.
Have you ever wanted to write a merb application and been turned off by the lack of merb/rSpec documentation? And if you are like me and have completely dumped Test::Unit and shudder at the idea that you might have to return to Test::Unit to reliably test an application written with the super-awesomely, fast(er) framework merb. Yeah. So totally me too!
After a few hours of headaches and digging through the rSpec and Test/Spec source and the sample MrBlog application on the merb site, I have a good platform to develop and test my merb application, reliably and as similar to the rails functionality as I can get it. So, without further ado and more discussion, here’s how I hacked it together:
First off, merb (0.3.7) loads all the plugins in both the dist (soon to be replaced by the directory app in 0.4.0) and the plugins directory at root (MERB_ROOT/plugins and DIST_ROOT/plugins) by requiring all the init.rb files in the directories. To get rSpec to work, you’ll probably need the rspec_on_merb plugin, by (as far as I can tell) Brian Takita. Feel free to put the plugin directory in either place, but for this app, I placed mine in the root. That becomes slightly important later.
I did this by creating the directory and typing
svn co http://svn.devjavu.com/merb/mrblog/trunk/plugins/rspec_on_merb/
.
After you have the plugin, we have to modify it a little. First, create an init.rb file in the plugin’s root (MERB_ROOT/plugins/rspec_on_merb/init.rb) and fill it out with the following:
dir = File.expand_path(File.dirname(__FILE__))
require "rubygems"
require "spec"
require "#{dir}/spec/spec_helper"
Dir["#{dir}/lib/*"].each do |file|
require file unless File.directory?(file)
end
Basically, this is going to load all the files in the plugin lib directory as well as the spec_helper, which you will definitely want. You can get rid of the files you won’t use, I didn’t use spec_suite at all, but you can use it if it suits you well.
Anyway, back to the process!
After that’s in place, you’ll want to modify the spec_helper in the directory (MERB_ROOT/plugins/spec/spec_helper.rb). The following is my entire spec_helper file. It should work for you right off the bat, but if not, let me know…
require "rubygems"
require "spec"
require "merb"
require 'active_record'
class SpecInitializer
attr_accessor :inst
module ClassMethods
def recreate_database_called?
@recreate_database_called
end
def recreate_database_called!
@recreate_database_called = true
end
def turn_off_printing
# TURN OFF PRINTING
$stdout = $stderr = File.open("/dev/null","a+");
end
def turn_on_printing
$stdout = $stderr = STDOUT
end
end
extend ClassMethods
def add_load_paths
$LOAD_PATH << "#{dir}/dist/app/controllers"
$LOAD_PATH << "#{dir}/dist/app/helpers"
$LOAD_PATH << "#{dir}/dist/app/models"
$LOAD_PATH << "#{dir}/dist/app/views"
end
def load_merb
require "#{dir}/../lib/spec/merb"
end
def run
@inst ||= self.class.new
@inst.add_load_paths
@inst.load_merb
@inst.initialize_database
@inst.require_project_files
@inst
end
def initialize_database
conn_options = YAML::load(Erubis::Eruby.new(IO.read("#{DIST_ROOT}/conf/database.yml")).result)
database = conn_options[:database]
username = conn_options[:username]
password = conn_options[:password]
unless recreate_database_called?
self.class.turn_off_printing
case conn_options[:adapter]
when 'mysql'
ActiveRecord::Base.connection.drop_database config[:database]
when /^sqlite/
FileUtils.rm_f File.join(RAILS_ROOT, config[:database])
when 'postgresql'
`dropdb "#{config[:database]}"`
end
self.class.turn_on_printing
end
# Connect to the database
ActiveRecord::Base.establish_connection conn_options["#{MERB_ENV}"]
unless recreate_database_called?
self.class.turn_off_printing
mig = []
Dir["#{DIST_ROOT}/schema/migrations/*"].each do |file|
require file; mig << File.basename(file, File.extname(file)).gsub(/^(d)+_/, '')
end
mig.each {|a| p a.classify.constantize}
mig.each {|a| a.classify.constantize.send :up}
self.class.turn_on_printing
end
# recreate_database_called! unless recreate_database_called?
end
def require_project_files
require_files_from_directory "controllers"
require_files_from_directory "helpers"
require_files_from_directory "models"
require_files_from_directory "views"
end
protected
def require_files_from_directory(app_dir)
Dir["#{dir}/dist/app/#{app_dir}/*"].each do |file|
require file
end
end
def recreate_database_called?
self.class.recreate_database_called?
end
def recreate_database_called!
self.class.recreate_database_called!
end
def dir
File.dirname(__FILE__)
end
end
I won’t go into detail as to what is going on, hopefully (and I suspect if you are using merb, you are somewhat confident with your rails understanding), but I will say that the output is suppressed ’cause we don’t want to see the database output everytime we spec, we just want to know if the spec passed or not. Feel free to add the printing in if you REALLY want to see it.
I added a custom directory in the plugin (MERB_DIR/rspec_on_merb/lib/custom) and that’s where I load the custom classes that I use like multipart.rb that you can find in the plugin’s helper currently. That’s for sure not necessary. I just like clean code bases. Anyway…
Now, I think we are done with the rspec_on_merb plugin directory. Let’s move to the test directory and get the specs going!
Make the following directory structure in your merb app (You can change this, of course, just pay attention to the require paths… if you change the directory, then you may have to change some of the require paths).
MERB_DIR/
…
test/
fixtures/
spec/
models/
controllers/
Okay, so now we are going to add all things you need to get the rSpec’s going green. In your spec directory (MERB_DIR/test/spec), add a file and call it spec_helper.rb (sound familiar? should).
The code I used for the spec_helper.rb is as follows
dir = File.expand_path(File.dirname(__FILE__))
$TESTING = true
$stdout = $stderr = File.open("/dev/null","a+")
require "rubygems"
require "spec"
require 'merb/merb_server'
dir = File.dirname(__FILE__)
Merb::Server.instance_eval {@@merb_raw_opts = []}
Merb::Server.merb_config
Merb::Server.config[:merb_root] = File.expand_path("#{dir}/../..")
Merb::Server.config[:dist_root] = File.expand_path("#{dir}/../../dist")
Merb::Server.config[:environment] = 'test'
require "merb"
require "active_record"
ActiveRecord::Base.verification_timeout = 14400
require "#{DIST_ROOT}/conf/merb_init"
require DIST_ROOT+"/conf/router.rb"
$LOAD_PATH << DIST_ROOT+"/app/models/"
$LOAD_PATH << DIST_ROOT+"/app/controllers/"
$LOAD_PATH << DIST_ROOT+"/app/helpers/"
Dir[DIST_ROOT+"/plugins/*/lib"].each { |m| $LOAD_PATH << m }
Dir[MERB_ROOT+"/plugins/*/lib"].each { |m| $LOAD_PATH << m }
require "spec/merb"
# Custom requires
require "#{dir}/login_service_helper"
$stdout = STDOUT
Spec::DSL::Behaviour.prepend_before(:each) { @si = SpecInitializer.new.run unless @no_refresh }
The important part of the spec_helper is the last line. That’s where you get the nice behavior that we are all used to from Rails. That’ll clear you database every test you run. Ugly, for sure.
In case you were wondering, the output (again) is surpressed in this block ’cause there are some print statements we don’t care about in there.
Okay, some more…
You definitely need a rake task. The one I use is as follows:
desc 'run specs'
task :specs => [:merb_init] do
Spec::Rake::SpecTask.new('specs') do |t|
t.spec_opts = ["--format", "specdoc"]
t.libs = ['lib', 'server/lib' ]
t.spec_files = FileList['test/spec/**/*_spec.rb']
end
end
I can’t remember (I’ve been hacking at this for a while) if the :merb_init task is apart of the Rakefile merb copies for you, so here is the one from my Rakefile
desc "load merb_init.rb"
task :merb_init do
require File.dirname(__FILE__)+'/dist/conf/merb_init.rb'
end
And I think you should be good to go…
This last tidbit bothered me for about an hour, so rather than have you dig around for it… if you are not familiar with the rSpec codebase, rSpec loads the specs and their inheritance tree based on the directory that your spec is located in. Unless you include the :behaviour_type => :controller on your controller specs, then you’ll get some pretty funky results. Like so:
describe Index, "controller", :behaviour_type => :controller do end
It’s probably good practice to do that for all your describe blocks, but the model code worked for me.
Anyway, let me know if this works for you…
UPDATE
I had to manipulate some of the plugin to allow for RESTful testing:
in the controller behavior file: (MERB_DIR/plugins/rspec_on_merb/lib/spec/merb/dsl/behaviour/controller.rb) I changed the post method to look like (only the change is posted, everything else should stay the same):
method = opts.delete(:request_method) || 'POST'
body, head = m.prepare_query(opts)
request = Spec::Merb::Fakes::FakeRequest.new({:request_uri => path, :path_info => path.sub(/?.*$/,'')}, method)
request['REQUEST_METHOD'] = method
request['CONTENT_TYPE'] = head
request['CONTENT_LENGTH'] = body.length
request.post_body = body
I added the request_method to the parameters so you can “fake” the method in your test, like such:
def do_put
@response, @controller = post "/users/1.xml", @params.merge({:request_method => "DELETE"})
end
I’ll continue to update my changes when/if I make ‘em!
6 comments ↓
Awesome! This is very important and I’m glad to see that you put the effort into making it work.
Hey just a quick one,
I changed:
File.basename(file, File.extname(file)).gsub(/^(d)+_/, ”)
To:
File.basename(file, File.extname(file)).gsub(/^(\d+)_/, ”)
To fix a file naming issue. I’m sure if it was an escaping issue when the article was posted.
Awesome post by the way.
Another quick change in the spec_helper.rb:
mig.each {|a| p a.camelize.constantize}
mig.each {|a| a.camelize.constantize.send :up}
Change constantize to camelize so that the migration names can be plural.
Thanks man!
I’ve fixed the article accordingly.
Trying to find some time to get it in the official plugin. I’ll try to get to it on this flight.
Anyway, thanks Carl
The current Merb trunk has changed quite a bit and it appears that the plugin has not been updated.
Are you using RSpec with a Merb trunk app at all? If not, I’d like to get it working properly so it will be ready for Merb 0.4.
I was using it with 0.3.7. And I actually went back to camping with mosquito because there were some funny things going on. I would love to do some work on it when I get a chance. It would be great if that was in by 0.4.0.
Leave a Comment