Tuesday, June 27, 2006

Ruby on Rails Unit Tests

Updated following multiple comments requesting examples and questions on where to put AR::Base subclass tests.

Everyone (who reads this blog anyway) knows that you should not cross boundaries while unit testing. Unfortunately, Ruby on Rails seems to believe otherwise. This is evident by the fact that the test:units rake task has the pre-requisite db:test:prepare. Additionally, if you use script/generate to create a model, it creates a [model].yml fixture file and a unit test that includes a call to the fixtures class method. Rails may be opinionated, but that doesn't mean I have to agree with it.

With a minor modification you can tell Rails not to run the db:test:prepare task. You should also create a new test helper that doesn't load the additional frameworks that you will not need. I found some of the code for this from reading a great book, Rails Recipes, by Chad Fowler.

You'll need to add a .rake file to /lib/tasks. The file contains one line:
Rake::Task[:'test:units'].prerequisites.clear
Additionally, you'll need to create a new helper file in /test. I named my file unit_test_helper.rb, but the file name is your choice.
ENV["RAILS_ENV"] = "test" 
require File.expand_path(File.dirname(__FILE__) + "/../config/environment")
require 'application'
require 'test/unit'
require 'action_controller/test_process'
require 'breakpoint'

class UnitTest
def self.TestCase
class << ActiveRecord::Base
def connection
raise InvalidActionError, 'You cannot access the database from a unit test', caller
end
end
Test::Unit::TestCase
end
end

class InvalidActionError < StandardError
end
As you can see, the unit_test_helper.rb only requires what is necessary; however, it also changes ActiveRecord::Base to throw an error if you attempt to access the connection from a unit test.

I included this test in my codebase to ensure expected behavior.
require File.dirname(__FILE__) + '/../unit_test_helper'

class AttemptToAccessDbThrowsExceptionTest < UnitTest.TestCase
def test_calling_the_db_causes_a_failure
assert_raise(InvalidActionError) { ActiveRecord::Base.connection }
end
end
Update (Again):
We have been using this style of testing for several months now and have over 130 tests at this point and our tests still run in less than a second.

This decision does carry some trade-offs though. First of all, it becomes a bit more work to test ActiveRecord::Base subclasses in your unit tests. I'm comfortable with the small amount of extra work since it results in a significantly faster running test suite.

Also, if you need to use a AR::Base class as a dependency for another class, you will need to mock or stub the AR::Base class. This generally requires using Dependency Injection or a framework such as Stubba. For example, if you have a method that returns an ActiveRecord::Base subclass you can mock the new call and return a stub instead.
class AccountInformationPresenter

def account
Account.new
end

end

class AccountInformationPresenterTest

def test_account_returns_a_new_account
Account.expects(:new).returns(stub(:name=>'jay'))
AccountInformationPresenter.new.account
end

end
In the above code, mocking the new call on Account prevents an unnecesary database trip.

For an example of what our unit tests look like here are some tests and the classes that the tests cover.
require File.dirname(__FILE__) + '/../../../unit_test_helper'

class SelectTest < Test::Unit::TestCase
def test_select_with_single_column
assert_equal 'select foo', Select[:foo].to_sql
end

def test_select_with_multiple_columns
assert_equal 'select foo, bar', Select[:foo, :bar].to_sql
end

def test_date_time_literals_quoted
date = DateTime.new(2006, 1, 20, 13, 30, 54)
assert_equal "select to_timestamp('2006-01-20 13:30:54', 'YYYY-MM-DD HH24:MI:SS')", Select[date].to_sql
end

def test_select_with_symbol_and_literal_columns
assert_equal "select symbol, 'literal'", Select[:symbol, 'literal'].to_sql
end

def test_select_with_single_table
assert_equal 'select foo from foo', Select[:foo].from[:foo].to_sql
end

def test_select_with_multiple_tables
assert_equal 'select column from bar, foo',
Select[:column].from[:foo, :bar].to_sql
end
end

require File.dirname(__FILE__) + '/../../unit_test_helper'

class TimeTest < Test::Unit::TestCase
def test_to_sql_gives_quoted
t = Time.parse('2006/05/01')
assert_equal "to_timestamp('2006-05-01 00:00:00', 'YYYY-MM-DD HH24:MI:SS')", t.to_sql
end

def test_to_pretty_datetime
d = Time.parse("05/10/2006")
assert_equal "05-10-2006 12:00 AM", d.to_pretty_time
end
end

class Select < SqlStatement
class << self
def [](*columns)
unless columns.select { |column| column.nil? }.empty?
raise "Empty Column in #{columns.inspect}"
end
self.new("select #{columns.collect{ |column| column.to_sql }.join(', ')}")
end
end

def from
@to_sql += " from "
self
end

def [](*table_names)
@to_sql += table_names.collect{ |table| table.to_s }.sort.join(', ')
self
end

end

class Time
def to_sql
"to_timestamp('" + formatted + "', 'YYYY-MM-DD HH24:MI:SS')"
end

def to_pretty_time
self.strftime("%m-%d-%Y %I:%M %p")
end

private

def formatted
year.to_s + "-" + month.pad + "-" + day.pad + " " + hour.pad + ":" + min.pad + ":" + sec.pad
end
end
Post a Comment