asplake

Wednesday, January 11, 2006

Revert that Rakefile!

To my slight embarrassment, I just noticed the following comments at the top of the Rails-generated Rakefile:

# Add your own tasks in files placed in lib/tasks ending in .rake
# for example lib/tasks/switchtower.rake, and they will automatically be available to Rake

So ignore my suggestion to edit your Rakefile, and create a lib/tasks/rcov.rake instead.

Combining the tips from the past couple of posts and tidying up a bit, mine looks like this:

require 'rake/clean'

# output goes to subdirectories of this one
RCOV_OUT = "coverage"

# "rake clobber" to remove output directory (along with other generated stuff)
CLOBBER.include(RCOV_OUT)

# don't report coverage on these files
RCOV_EXCLUDE = %w(boot.rb environment.rb).join(',')

# RCOV command, run as though from the commandline.  Amend as required or perhaps move to config/environment.rb?
RCOV = "/ruby/bin/ruby /ruby/bin/rcov --no-color --exclude #{RCOV_EXCLUDE}"

desc "generate a unit test coverage report in coverage/unit; see coverage/unit/index.html afterwards"
task :coverage_units do
  sh "#{RCOV} --output #{RCOV_OUT}/unit test/unit_tests.rb"
end

desc "generate a functional test coverage report in coverage/functional; see coverage/functional/index.html afterwards"
task :coverage_functional do
  sh "#{RCOV} --output #{RCOV_OUT}/functional test/functional_tests.rb"
end

desc "generate a coverage report for unit and functional tests together in coverage/all; see coverage/all/index.html afterwards"
task :coverage_all do
  sh "#{RCOV} --output #{RCOV_OUT}/all test/all_tests.rb"
end

desc "equivalent to coverage_all"
task :coverage => [:coverage_all]

Technorati tags:

Friday, January 06, 2006

100% test coverage with rcov

Further to my previous post, coverage is now at 100%.

Fixing that last 4% was mostly a matter of:

  • finishing the writing of my tests (obviously). The missing ones were for the post actions in my main controller
  • excluding the standard config files boot.rb and environment.rb from the report, the former because of some standard platform-specific stuff (unreachable in my setup) and the second because (bizarrely) it reported that I wasn't achieving full coverage of its comments! Added --exclude boot.rb,environment.rb as the first argument to rcov.

There was also an issue with code of the following form:

  x = y.collect do |i|
    i.to_s
  end.join("/")

For some reason, rcov failed to recognise the execution of the last line. I had good reason to rewrite this, so I haven't investigated this any further.

Technorati tags:

Wednesday, January 04, 2006

Test coverage with rcov and rake (96.2% at first attempt)

Last night I installed the rcoverage gem (recommended in Agile Web Development with Rails - there I go again!), only to discover in its README that its author recommends a switch to rcov.

Neither tool seems to be (shall we say) over-documented, but I did find Ruby On Rails Test Coverage in Alex Pooley's blog. I followed his instructions and soon had a report on one of my unit tests up and running.

As you may guess from Alex's article, rcov doesn't allow you to specify multiple test programs to run, so you need to maintain a script that will run the whole suite for you.

All that's needed is to require each test script, but I'm far too lazy (not to mention error-prone) to maintain lists of tests manually. Instead, here are three scripts I placed in my tests/ directory, to run unit tests, functional tests and both together.

test/unit_tests.rb:

  # Run all unit tests
    Dir[File.dirname(__FILE__) + "/unit/*_test.rb"].each do |f|
    require f
  end

test/functional_tests.rb:

  # Run all functional tests
  Dir[File.dirname(__FILE__) + "/functional/*_test.rb"].each do |f|
    require f
  end

test/all_tests.rb:

  # Combine unit and functional tests in one run
  %w( unit_tests functional_tests ).each do |f|
    require File.dirname(__FILE__) + "/#{f}.rb"
  end

Finally, I added the following to my project's Rakefile:

  # output directory - removed with "rake clobber" (needs a "require 'rake/clean'" above)
  CLOBBER.include("coverage")

  # RCOV command, run as though from the commandline.  Amend as required or perhaps move to config/environment.rb?
  RCOV = "/ruby/bin/ruby /ruby/bin/rcov"

  desc "generate a unit test coverage report in coverage/unit; see coverage/unit/index.html afterwards"
  task :coverage_units do
    sh "#{RCOV} --output coverage/unit test/unit_tests.rb"
  end

  desc "generate a functional test coverage report in coverage/functional; see coverage/functional/index.html afterwards"
  task :coverage_functional do
    sh "#{RCOV} --output coverage/functional test/functional_tests.rb"
  end

  desc "generate a coverage report for unit and functional tests together in coverage/all; see coverage/all/index.html afterwards"
  task :coverage_all do
    sh "#{RCOV} --output coverage/all test/all_tests.rb"
  end

  desc "equivalent to coverage_all"
  task :coverage => [:coverage_all]

The "desc" lines result in helpful output if I run "rake --tasks". I can never remember them all!

Running the overall coverage report is now as easy as "rake coverage", viewed by pointing my browser at coverage/all/index.html.

For the record, without changing any of my tests, I get an overall coverage rate of 96.2%, which came as quite a pleasant surprise.

One final word of warning: expect tests to run something like 2 orders of magnitude slower when running under rcov. Mine took nearly 5 minutes...

Technorati tags:

Monday, January 02, 2006

40 tests, 115 assertions, 0 failures, 0 errors

I have a fairly complete set of unit tests for my model now. I found one bug in the process, and tidied up my API a bit when I found the tests themselves looking clunky. Coverage is good enough now that I can change something and rely on the tests to pick up any knock-on changes that I've missed.

My only caveat is that I have significantly more test code than model code now, and I might have to redress this a bit with some refactoring. I'm thinking along the lines of some helpers for testing STI, acts_as_list, :dependent => :destroy, etc.

I made a start today on functional tests. With unit testing, I was familiar enough with the concepts but new to doing it in Ruby and with Rails (both of which make it really easy). With functional testing, I'm not too sure where to stop - I'm aiming to test most or all paths through the controllers, but how thoroughly should I test the views? Perhaps a re-read of Mike Clark's chapter in the excellent Agile Web Development with Rails is in order.

Technorati tag:

Saturday, December 31, 2005

STI followup: unit tests

I'm a complete newbie when it comes to unit testing in Ruby (I started only today), but here's a unit test for yesterday's STI example.

Test data (in test/fixtures/element_defs.yml) below. Note that the first entry root_name_def is subclassed to RefComponentDef.

  root_name_def:
    type: RefComponentDef
    id: 1
  root_description_def:
    id: 2

In my tests for the parent class ElementDef it looks like I'm disregard subclassing altogether, but passing this test shows implicitly that subclasses and their objects load fine together:

  class ElementDefTest < Test::Unit::TestCase
    fixtures :element_defs
  
    # Check that the fixture data makes sense
    def test_fixtures
      assert_kind_of ElementDef, element_defs(:root_name_def)
      assert_kind_of ElementDef, element_defs(:root_description_def)
   end
  
    # Check that the loaded data makes sense
    def test_loaded_data
      assert_kind_of ElementDef, ElementDef.find_by_id(element_defs(:root_name_def)[:id])
      assert_kind_of ElementDef, ElementDef.find_by_id(element_defs(:root_description_def)[:id])
    end
 

In the subclass's tests I show that its objects can be read via the superclass's find methods:

  class RefComponentDefTest < Test::Unit::TestCase
    fixtures :element_defs

    # Check the ElementDef fixture data correctly identifies :root_name_def as a RefComponentDef
    def test_fixtures
      assert_kind_of RefComponentDef, element_defs(:root_name_def)
    end
  
    # Check that a RefComponentDef can be read back from the database via its superclass
    def test_loaded_data
       assert_kind_of RefComponentDef, ElementDef.find_by_id(element_defs(:root_name_def)[:id])
    end
  end
I could add this to the wiki - did anyone find this useful?

Technorati tag:

Friday, December 30, 2005

Loading model classes with STI

I found out the hard way that the SQL generated by Single Table Inheritance (STI) depends on what Rails knows to be the subclasses of the class you attempt to query. If the subclasses don't happen to be loaded, the query doesn't return all the results you'd expect. The simple cure is to add dependencies in your ApplicationController. You can do this more than once, eg:
  class ApplicationController < ActionController::Base
    # Make sure that ActiveRecord is always aware of Entry's subclasses
    model :entry, :catalog, :root_catalog

    # Make sure that ActiveRecord is always aware of ElementDef's subclasses
    model :element_def, :ref_component_def
  end
Spookily, having decided to write this up here and (later tonight) on the Rails wiki at WhenToUseTheModelMethod, I see this raised as a bug today. See ticket 3358. Personally, I think this is a feature rather than a bug, but I suppose it is slightly weird that a superclass should be dependent on its subclasses. Technorati tag:

Tuesday, December 20, 2005

Here goes...

...because I want to write something non-boilerplate in Rails, and the world clearly needs another Content Management System ;-) I'll post the odd tip or trick here as I go.