RSpec, Autotest, and Ruby-Libnotify
Posted March 3rd, 2008 in ProgrammingTags: Linux, Programming, Ruby on Rails
After many false starts, I decided that I really needed to wrap my head around unit testing when writing Rails applications. I more or less completed a Rails 1.2.3 application without any formal tests, and I would like to upgrade it to 2.0.2 and make it RESTful in the process. At the same time, I’ve moved from a Windows development environment to a *nix one after installing Xubuntu on my laptop (an old Compaq Presario). I’ve also switched from Cream to Emacs. Despite my Windows desktop being more than twice as fast as my laptop, I just could not stand not being in a true *nix environment. Too much of the Windows idiosyncrasies got on my nerves. And my switch from Cream to Emacs was because I just didn’t like the instability of the hacks required to make Vim less of a modal editor. If I tire of Emacs, I may try pure Vim instead, but I remember installing Cream simply because I didn’t like pure Vim to start with. So with these various changes going on with my Rails programming environment, I figured it was an ideal time to learn to formally test my applications. Of course the first part is setting up the testing environment so that it is easy to use, stays out of your way, and is informative.
RSpec is the current black when it comes to testing. Seeing how it’s adopted by merb by default and I’m targetting merb for an old app I shelved because Rails just didn’t have the high performance yet to pull it off, I figured it would be the test method I should learn. It also helps that RSpec is just more readable than Test::Unit. Installation is more or less trivial and is covered succinctly in the RSpec documentation. Just remember to install RSpec as a plugin and not as a gem. Autotest will puke otherwise. You can also use the -x
option if you want to install it as an svn external. (Another option would be piston another gem I need to look at.)
ruby script/plugin install http://rspec.rubyforge.org/svn/tags/CURRENT/rspec ruby script/plugin install http://rspec.rubyforge.org/svn/tags/CURRENT/rspec_on_rails
Then you bootstrap the plugin which installs the generators and rake tasks.
ruby script/generate rspec ruby script/generate rspec_model foo rake db:migrate rake spec
If all goes well, rspec should run and you should get a result similar to this:
. Finished in 0.313 seconds 1 example, 0 failures
Now that RSpec is running properly, we want it to run automatically. That’s where ZenTest comes in. (The capitalization is intentional.)
sudo gem install ZenTest
ZenTest contains autotest. You start autotest from the root of your Rails application.
autotest
Autotest will then sit there continually checking your app folders for any file changes. When one is detected that requires RSpec to run, autotest will fire off the rake task to do so. Go ahead and edit ./app/models/foo.rb
by adding a blank line and then save it. When you return to the terminal window you should see that the test ran and you got the same result as before.
There is a prettier and more informative alternative than the results provided in the terminal. RSpec can produce HTML output as well. Create a folder ./doc/spec/
for the output folder. Now open up ./spec/spec.opts
in your editor and add the following lines.
--format html:doc/spec/report.html
This will cause an HTML report to be generated every time a test is run, in addition to the terminal output. The HTML report not only tells you what tests failed and passed but also where the failures occurred. The HTML format is much easier to navigate than the terminal output.
Mac OS X users have a notification program called Growl that autotest hooks into. On Windows there’s Snarl. For Linux, there’s libnotify. libnotify is not a standalone application like Growl or Snarl, but a library around which an application, or in this case, an applet must be written. There’s a handful of how-tos that walk you thru hooking autotest to libnotify, but the one I considered the best was this blog post, Gnome autotest Notifications. If you follow those instructions, you should get a working notifier, but I would recommend replacing his .autotest file with my .autotest file:
require 'rnotify' require 'gtk2' require 'launchy' module Autotest::RNotify class Notification attr_accessor :verbose, :image_root, :tray_icon, :notification, :image_pass, :image_pending, :image_fail, :image_file_pass, :image_file_pending, :image_file_fail, :status_image_pass, :status_image_pending, :status_image_fail def initialize(timeout = 5000, image_root = "#{ENV['HOME']}/.autotest_images" , report_url = "doc/spec/report.html", verbose = false) self.verbose = verbose self.image_root = image_root self.image_file_pass = "#{image_root}/pass.png" self.image_file_pending = "#{image_root}/pending.png" self.image_file_fail = "#{image_root}/fail.png" raise("#{image_file_pass} not found") unless File.exists?(image_file_pass) raise("#{image_file_pending} not found") unless File.exists?(image_file_pending) raise("#{image_file_fail} not found") unless File.exists?(image_file_fail) puts 'Autotest Hook: loading Notify' if verbose Notify.init('Autotest') || raise('Failed to initialize Notify') puts 'Autotest Hook: initializing tray icon' if verbose self.tray_icon = Gtk::StatusIcon.new tray_icon.pixbuf = Gdk::Pixbuf.new(image_file_pending,22,22) tray_icon.tooltip = 'RSpec Autotest' puts 'Autotest Hook: Creating Notifier' if verbose self.notification = Notify::Notification.new('X', nil, nil, tray_icon) notification.timeout = timeout puts 'Autotest Hook: Connecting mouse click event' if verbose tray_icon.signal_connect("activate") do Launchy::Browser.new.visit(report_url) end Thread.new { Gtk.main } sleep 1 tray_icon.embedded? || raise('Failed to set up tray icon') end def notify(icon, tray, title, message) notification.update(title, message, nil) notification.pixbuf_icon = icon tray_icon.tooltip = "Last Result: #{message}" tray_icon.pixbuf = tray notification.show end def passed(title, message) self.image_pass ||= Gdk::Pixbuf.new(image_file_pass, 48, 48) self.status_image_pass ||= Gdk::Pixbuf.new(image_file_pass, 22, 22) notify(image_pass, status_image_pass, title, message) end def pending(title, message) self.image_pending ||= Gdk::Pixbuf.new(image_file_pending, 48, 48) self.status_image_pending ||= Gdk::Pixbuf.new(image_file_pending, 22, 22) notify(image_pending, status_image_pending, title, message) end def failed(title, message) self.image_fail ||= Gdk::Pixbuf.new(image_file_fail, 48, 48) self.status_image_fail ||= Gdk::Pixbuf.new(image_file_fail, 22, 22) notify(image_fail, status_image_fail, title, message) end def quit puts 'Autotest Hook: Shutting Down...' if verbose #Notify.uninit Gtk.main_quit end end Autotest.add_hook :initialize do |at| @notify = Notification.new end Autotest.add_hook :ran_command do |at| results = at.results.last unless results.nil? output = results[/(\d+)\s+examples?,\s*(\d+)\s+failures?(,\s*(\d+)\s+pending)?/] if output failures = $~[2].to_i pending = $~[4].to_i end if failures > 0 @notify.failed("Tests Failed", output) elsif pending > 0 @notify.pending("Tests Pending", output) else unless at.tainted @notify.passed("All Tests Passed", output) else @notify.passed("Tests Passed", output) end end end end Autotest.add_hook :quit do |at| @notify.quit end end
The improvements I made are to (a) forego using themeable icons and stick strictly with the Autotest Growl Fail/Pass Smilies (b) run a check to make sure the images exist otherwise raise an error, and © load the RSpec HTML report when you click the icon in the system tray. In order for the onclick event to work, you need to install the launchy gem.
sudo gem install launchy
After all that is up and running, you should have a fully functioning test environment that will automatically run (after starting autotest in a terminal) with a notification applet running in your system tray that you can click to load a well-formatted HTML report in your browser.
In your description about setting up the spec.opts file, if you only include ‘–format html:doc/spec/result.html‘, you’re notification icon will not change or report the status. You have to add the following line in to both generate html and have the notification icon work properly: ‘–format progress‘