After many false starts, I decided that I really needed to wrap my head around unit test­ing when writ­ing Rails appli­ca­tions. I more or less com­pleted a Rails 1.2.3 appli­ca­tion with­out any for­mal tests, and I would like to upgrade it to 2.0.2 and make it REST­ful in the process. At the same time, I’ve moved from a Win­dows devel­op­ment envi­ron­ment to a *nix one after installing Xubuntu on my lap­top (an old Com­paq Pre­sario). I’ve also switched from Cream to Emacs. Despite my Win­dows desk­top being more than twice as fast as my lap­top, I just could not stand not being in a true *nix envi­ron­ment. Too much of the Win­dows idio­syn­crasies got on my nerves. And my switch from Cream to Emacs was because I just didn’t like the insta­bil­ity of the hacks required to make Vim less of a modal edi­tor. If I tire of Emacs, I may try pure Vim instead, but I remem­ber installing Cream sim­ply because I didn’t like pure Vim to start with. So with these var­i­ous changes going on with my Rails pro­gram­ming envi­ron­ment, I fig­ured it was an ideal time to learn to for­mally test my appli­ca­tions. Of course the first part is set­ting up the test­ing envi­ron­ment so that it is easy to use, stays out of your way, and is informative.

RSpec is the cur­rent black when it comes to test­ing. See­ing how it’s adopted by merb by default and I’m tar­get­ting merb for an old app I shelved because Rails just didn’t have the high per­for­mance yet to pull it off, I fig­ured it would be the test method I should learn. It also helps that RSpec is just more read­able than Test::Unit. Instal­la­tion is more or less triv­ial and is cov­ered suc­cinctly in the RSpec doc­u­men­ta­tion. Just remem­ber to install RSpec as a plu­gin and not as a gem. Autotest will puke oth­er­wise. You can also use the -x option if you want to install it as an svn exter­nal. (Another option would be pis­ton 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 boot­strap the plu­gin which installs the gen­er­a­tors 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 sim­i­lar to this:

.
Finished in 0.313 seconds
1 example, 0 failures

Now that RSpec is run­ning prop­erly, we want it to run auto­mat­i­cally. That’s where Zen­Test comes in. (The cap­i­tal­iza­tion is intentional.)

sudo gem install ZenTest

Zen­Test con­tains autotest. You start autotest from the root of your Rails application.

autotest

Autotest will then sit there con­tin­u­ally check­ing your app fold­ers 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 ter­mi­nal win­dow you should see that the test ran and you got the same result as before.

There is a pret­tier and more infor­ma­tive alter­na­tive than the results pro­vided in the ter­mi­nal. RSpec can pro­duce HTML out­put as well. Cre­ate a folder ./doc/spec/ for the out­put folder. Now open up ./spec/spec.opts in your edi­tor and add the fol­low­ing lines.

--format 
html:doc/spec/report.html

This will cause an HTML report to be gen­er­ated every time a test is run, in addi­tion to the ter­mi­nal out­put. The HTML report not only tells you what tests failed and passed but also where the fail­ures occurred. The HTML for­mat is much eas­ier to nav­i­gate than the ter­mi­nal output.

Mac OS X users have a noti­fi­ca­tion pro­gram called Growl that autotest hooks into. On Win­dows there’s Snarl. For Linux, there’s lib­no­tify. lib­no­tify is not a stand­alone appli­ca­tion like Growl or Snarl, but a library around which an appli­ca­tion, or in this case, an applet must be writ­ten. There’s a hand­ful of how-tos that walk you thru hook­ing autotest to lib­no­tify, but the one I con­sid­ered the best was this blog post, Gnome autotest Noti­fi­ca­tions. If you fol­low those instruc­tions, you should get a work­ing noti­fier, but I would rec­om­mend replac­ing 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 improve­ments I made are to (a) forego using the­me­able icons and stick strictly with the Autotest Growl Fail/Pass Smi­lies (b) run a check to make sure the images exist oth­er­wise raise an error, and © load the RSpec HTML report when you click the icon in the sys­tem 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 run­ning, you should have a fully func­tion­ing test envi­ron­ment that will auto­mat­i­cally run (after start­ing autotest in a ter­mi­nal) with a noti­fi­ca­tion applet run­ning in your sys­tem tray that you can click to load a well-formatted HTML report in your browser.

One Response to “RSpec, Autotest, and Ruby-Libnotify”

  1. sonny garcia said on July 9th, 2008 at 9:34 pm:

    In your descrip­tion about set­ting up the spec.opts file, if you only include ‘–for­mat html:doc/spec/result.html‘, you’re noti­fi­ca­tion icon will not change or report the sta­tus. You have to add the fol­low­ing line in to both gen­er­ate html and have the noti­fi­ca­tion icon work prop­erly: ‘–for­mat progress‘