2009-05-21

Ruby on Rails 2.3 unit tests

The blog post Ruby on Rails Unit Tests explains why it is a bad idea to give the unit tests access to the database, and how to set up your Rails project so that you get an exception if you try to access the database with ActiveRecord model objects from a unit test. Unfortunately, that blog post doesn't work with the latest Rails. This blog post explains how to set up a Ruby on Rails 2.3 project to disallow database accees in the unit tests. The instructions were tested with Rails 2.3.2.

Step 1. Create file lib/tasks/unit_tests.rake with the following contents:
Rake::Task[:'test:units'].prerequisites.delete('db:test:prepare')
Step 2. Create file test/unit_test_helper.rb with the following contents:
# similar to autogenerated test/test_helper.rb
#
fail "some of the unit tests has loaded test_helper.rb. Please change " +
"(require 'test_helper') to (require 'unit_test_helper') in " +
"tests/unit/**/*.rb" if $".include?('test_helper.rb')
fail "some of the unit tests has loaded test_help.rb. Please make sure that " +
"the first line is (require 'unit_test_helper') in tests/unit/**/*.rb" if
$".include?('test_help.rb')
require File.expand_path(File.dirname(__FILE__) + "/../config/environment")
HIDE_ActiveRecord = self.class.send(:remove_const, :ActiveRecord)
require 'test_help' # rails 2.3.2 standard module
ActiveRecord = self.class.send(:remove_const, :HIDE_ActiveRecord)

class FakeConnection
class InvalidActionError < StandardError
end
COLUMNS = {}
def self.columns(table_name, name=nil)
if COLUMNS.has_key?(table_name)
COLUMNS[table_name]
else
raise InvalidActionError, "please create something like this first: " +
"FakeConnection::COLUMNS[#{table_name.inspect}] = [ " +
"ActiveRecord::ConnectionAdapters::Column.new(name=..., " +
"default=nil, sql_type=\"text\", null=false), ...]"
end
end
DB_ERROR_MSG = 'You cannot access the database from a unit test'
def self.quote_table_name(*args) # called from ActiveRecord::Base.find
raise InvalidActionError, DB_ERROR_MSG, caller
end
def self.quote_column_name(*args) # called from ActiveRecord::Base.delete
raise InvalidActionError, DB_ERROR_MSG, caller
end
def self.select_all(*args) # called from ActiveRecord::Base.find_by_sql
raise InvalidActionError, DB_ERROR_MSG, caller
end
def self.transaction(*args) # called from ActiveRecord::Base.save
raise InvalidActionError, DB_ERROR_MSG, caller
end
end
class << ActiveRecord::Base
def connection
FakeConnection
end
end
Step 3. Make sure all your unit tests (i.e. =*.rb= files in =test/unit=, recursively) start with the line require 'unit_test_helper' instead of require 'test_helper'. Don't forget any of the =*.rb= files, otherwise you'll get an error message at test file load time.

Step 4. If you need access to your model objects, add the necessary column declaration to your test file. For example, for model class Foo:
FakeConnection::COLUMNS['foos'] = [
ActiveRecord::ConnectionAdapters::Column.new(
name="bar1", default=nil, sql_type="varchar(255)", null=false),
ActiveRecord::ConnectionAdapters::Column.new(
name="bar2", default=nil, sql_type="integer", null=false),
]
After this, you can do a Foo.new in your tests – but you won't be able to save the object, and Foo.find, Foo.find_by_sql and Foo.delete won't work either. If you try any of those, a FakeConnection::InvalidAction gets raised. To test such a functionality, use an integration test instead of a unit test. To do so, create your test file as test/integration/*.rb instead of test/unit/*.rb.

Once all steps are done, run your unit tests with rake test:units. This will run all tests in all *.rb files in the test/units directory (recursively). If you get an exception, and you need the full backtrace, rerun with BACKTRACE=1 rake test:units. For an even longer backtrace, run rake --trace test:units.

No comments: