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 (c) 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.

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

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

    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`

Leave a Reply