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: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, you cannot test your ActiveRecord::Base subclasses in your unit tests. I'm comfortable with this since the AR::Base classes hit the database, thus making them perfect candidates for functional tests.
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 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