Ruby on Rails
HowtoUseMultipleDatabasesWithFixtures (Version #45)

Using instantiated fixtures with multiple databases without modifying AR

RoR testing with instantiated fixtures while using multiple databases is more possible then ever. All you need to do is just implement your own test_helper method, that load fixtures into the databases and instatiate them. Here’s my code, which i use in my project(just copy it into test_helper.rb):


cattr_accessor :classes_cache
  #class cache for storing already founded classes from models
  @@classes_cache = {}
  
  def load_user_fixtures(*table_names)
    fixtures = {}
    table_names = table_names.flatten.collect{|t| t.to_s}
    table_names.each do |table_name|
      unless @@classes_cache[table_name].nil?
        klas = @@classes_cache[table_name]
      else
        begin 
          #try to find class name from table name
          klas = eval(table_name.classify)
        rescue
          #go to model directory, run through all models and search for table name
          classes = Dir.entries(RAILS_ROOT + "/app/models").select{|d| d.include?(".rb")}.collect{|f| File.basename(f, ".rb").classify}
          klas_names = classes.select{|f| (eval("#{f}.table_name") rescue false)==table_name }
          klas_name = klas_names.blank? ? table_name.classify : klas_names.first
          klas = eval(klas_name)
        end
        @@classes_cache[table_name] = klas
      end
      #load fixture
      fixtures[table_name] = Fixtures.create_fixtures(File.dirname(__FILE__) + "/fixtures", table_name, 
                                                      {table_name.to_sym => klas.name}){klas.connection}
    end
    #run through all fixtures and instantiate them
    fixtures.each_pair do |table_name, fixs|
      Fixtures.instantiate_fixtures(self, table_name, fixs)
    end
  end

Disable transactional and instantiated fixtures

self.use_transactional_fixtures = false
self.use_instantiated_fixtures  = false

To load your fixtures in tests use implemented helper method in your unit or functional tests like this(notice, that load_user_fixtures is instance method, but regular fixtures method is class method)


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

class EmployeeTest < Test::Unit::TestCase
def setup
load_user_fixtures :employees, :users
end
end

AR has a small bug when asking for table_name to class, which is inherited from abstract class. So we have to set table names even if they are the same as model name.

First set abstract class as usual to define connection.


class AnotherDatabaseConnector < ActiveRecord::Base
  self.abstract_class = true
  establish_connection "another_database_#{RAILS_ENV}" 
  # all subclasses use connection from here
end

All your subclasses accessing another database will look like this. We have to set table name for those, inherited from our abstract class.

class User < AnotherDatabaseConnector
  set_table_name "users" # we need to set this explicitly
end

I done this, because i had many tests already done, and i was too lazy to rewrite them for non-instantiated fixtures, and it works, you don’t have to change anything in AR, you don’t have to set DB connections in tests, all you have to do is just use this method to load fixtures instead of default method fixtures.

If you have any problems or suggestions, you can discuss email me robert.cigan@skvely.cz, or chat directly using ICQ 82645774

- rimmer


Unit Testing With Multiple Databases Without Modifying ActiveRecord.rb

Connecting and Unit Testing with Multiple Databases

After two days of reading the wiki and experimenting I was able to access two databases (legacy: MS SQLServer and app: MySQL) and create unit tests and test fixtures for both. Here’s what i did:

Add Additional Records to database.yml

Add an additional entry to the database configuration for the legacy database (one entry for each environment test, development, production).


 legacy_test:
   adapter: sqlserver
   host: (local)
   database: legacy_test
   username: user
   password: password

Create Base Model Class For Legacy Classes

I created a base legacy class which extends ActiveRecord::Base and established the connection to the legacy database there.


class LegacyBase < ActiveRecord::Base
	self.abstract_class = true
    establish_connection("legacy_#{RAILS_ENV}")
end

All model classes that access the legacy database extend the legacy base class.


class LegacyWeb < LegacyBase
end

Note If you use ruby script/create model some model make sure you edit the generated class and replace the generated parent class (MyClass < ActiveRecord::Base) with the LegacyBase class as shown above.

Explicitly Load Fixtures In Unit Tests for Legacy Classes

For legacy classes, fixtures must be loaded using Fixture.create_fixtures, can’t use the standard fixture: some_fixture


class WebTest < Test::Unit::TestCase
#  fixtures :webs, :lists	# Can't use this when using multiple databases

 	def setup
		ActiveRecord::Base.connection = LegacyBase.connection
		Fixtures.create_fixtures(File.dirname(__FILE__) + "/../fixtures", "webs"){LegacyBase.connection}		
		
		@web = Web.find("abc")
 	end
 
  
  # Replace this with your real tests.
  def test_truth
    assert_kind_of Web, @web

  end
end

Note I found that the ActiveRecord::Base.connection attribute had to be set otherwise when the test fixtures are inserted during the unit test runs, the column names in the SQL were being formatted with the default test database adapter’s formatting. In my case the mysql_adapter was adding “’” around each column name in the insert statement causing an invalid sql error when trying to insert into the legacy MS SQLServer database.

Explicitly Load Fixtures In Unit Tests for Regular Classes

Unit testing for model classes that are not derived from the LegacyBase class and connect to the default test database require the connection to be explicitly established and the fixtures loaded in a similar fashion to the legecy unit test classes. If the connection is not explicitly specified the loading of fixture data may use the connection specified in the legacy unit tests.


  def setup
  	ActiveRecord::Base.establish_connection("#{RAILS_ENV}")
  	Fixtures.create_fixtures(File.dirname(__FILE__) + "/../fixtures", "scrum_foos")
  end

Note The connection does not need to be specified for the create_fixtures call

Thanks to everyone who contributed to the multiple database topics on the wiki HowtoUseMultipleDatabases, couldn’t have figured this out without all of the good information.

- JSzod


I followed the previous solutions, but couldn’t figure out how to clone the structure from “legacy_development” to “legacy_test”.

I came up with a solution to use your legacy databases with your main test database. It has the benefit that fixtures will work as usual, but will break if you have tables with the same name in different lagacy databases. You need legacy.rake and base.rb below. Use at your own risk! I’m a newbie just hacking away. :)

Run with: rake db:test:prepare:legacy

# lib/tasks/legacy.rake
# 
# - prepare all legacy databases with rake db:test:prepare:legacy
# - db:test:*:legacy_db to run any db:test: task on database legacy_db
# 
# list your legacy dbs here:
legacy_dbs = %w(ciministry)

def prepend(prefix, array)
  array.collect { |item| prefix + item }
end

namespace :db do
  namespace :test do    
    namespace :prepare do

      desc "like db:test:prepare plus copies all legacy database structure to the test database" 
      task :legacy => "db:test:prepare" do
        prepend("db:test:clone_structure:", legacy_dbs).each do |task|
          Rake::Task[task].invoke
        end
      end
      
    end
    commands = %w(clone clone_structure)
    commands.each do |command|
      legacy_dbs.each do |legacy_db|
        namespace command do
        
          desc "like db:test:#{command} but for #{legacy_db}" 
          task legacy_db do
            config_path = "config/environments" 

            # db:test:clone will complain without this environments file
            File.copy("#{config_path}/#{RAILS_ENV}.rb", "#{config_path}/#{legacy_db}.rb")

            # fake the environment to get the legacy database's structure in the test db
            backup_env = RAILS_ENV;
            RAILS_ENV = legacy_db
            Rake::Task["db:test:#{command}"].invoke
            
            # restore
            File.delete("#{config_path}/#{legacy_db}.rb")
            RAILS_ENV = backup_env;
          end

        end
      end
    end
  end
end

Use this as your abstract base instead of the one presented above

class LegacyBase < ActiveRecord::Base
  self.abstract_class = true
  
  if RAILS_ENV == "test"
    establish_connection("test")
  else
    establish_connection("legacydb")
  end
end


- AndrewRoth

Using instantiated fixtures with multiple databases without modifying AR

RoR testing with instantiated fixtures while using multiple databases is more possible then ever. All you need to do is just implement your own test_helper method, that load fixtures into the databases and instatiate them. Here’s my code, which i use in my project(just copy it into test_helper.rb):


cattr_accessor :classes_cache
  #class cache for storing already founded classes from models
  @@classes_cache = {}
  
  def load_user_fixtures(*table_names)
    fixtures = {}
    table_names = table_names.flatten.collect{|t| t.to_s}
    table_names.each do |table_name|
      unless @@classes_cache[table_name].nil?
        klas = @@classes_cache[table_name]
      else
        begin 
          #try to find class name from table name
          klas = eval(table_name.classify)
        rescue
          #go to model directory, run through all models and search for table name
          classes = Dir.entries(RAILS_ROOT + "/app/models").select{|d| d.include?(".rb")}.collect{|f| File.basename(f, ".rb").classify}
          klas_names = classes.select{|f| (eval("#{f}.table_name") rescue false)==table_name }
          klas_name = klas_names.blank? ? table_name.classify : klas_names.first
          klas = eval(klas_name)
        end
        @@classes_cache[table_name] = klas
      end
      #load fixture
      fixtures[table_name] = Fixtures.create_fixtures(File.dirname(__FILE__) + "/fixtures", table_name, 
                                                      {table_name.to_sym => klas.name}){klas.connection}
    end
    #run through all fixtures and instantiate them
    fixtures.each_pair do |table_name, fixs|
      Fixtures.instantiate_fixtures(self, table_name, fixs)
    end
  end

Disable transactional and instantiated fixtures

self.use_transactional_fixtures = false
self.use_instantiated_fixtures  = false

To load your fixtures in tests use implemented helper method in your unit or functional tests like this(notice, that load_user_fixtures is instance method, but regular fixtures method is class method)


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

class EmployeeTest < Test::Unit::TestCase
def setup
load_user_fixtures :employees, :users
end
end

AR has a small bug when asking for table_name to class, which is inherited from abstract class. So we have to set table names even if they are the same as model name.

First set abstract class as usual to define connection.


class AnotherDatabaseConnector < ActiveRecord::Base
  self.abstract_class = true
  establish_connection "another_database_#{RAILS_ENV}" 
  # all subclasses use connection from here
end

All your subclasses accessing another database will look like this. We have to set table name for those, inherited from our abstract class.

class User < AnotherDatabaseConnector
  set_table_name "users" # we need to set this explicitly
end

I done this, because i had many tests already done, and i was too lazy to rewrite them for non-instantiated fixtures, and it works, you don’t have to change anything in AR, you don’t have to set DB connections in tests, all you have to do is just use this method to load fixtures instead of default method fixtures.

If you have any problems or suggestions, you can discuss email me robert.cigan@skvely.cz, or chat directly using ICQ 82645774

- rimmer


Unit Testing With Multiple Databases Without Modifying ActiveRecord.rb

Connecting and Unit Testing with Multiple Databases

After two days of reading the wiki and experimenting I was able to access two databases (legacy: MS SQLServer and app: MySQL) and create unit tests and test fixtures for both. Here’s what i did:

Add Additional Records to database.yml

Add an additional entry to the database configuration for the legacy database (one entry for each environment test, development, production).


 legacy_test:
   adapter: sqlserver
   host: (local)
   database: legacy_test
   username: user
   password: password

Create Base Model Class For Legacy Classes

I created a base legacy class which extends ActiveRecord::Base and established the connection to the legacy database there.


class LegacyBase < ActiveRecord::Base
	self.abstract_class = true
    establish_connection("legacy_#{RAILS_ENV}")
end

All model classes that access the legacy database extend the legacy base class.


class LegacyWeb < LegacyBase
end

Note If you use ruby script/create model some model make sure you edit the generated class and replace the generated parent class (MyClass < ActiveRecord::Base) with the LegacyBase class as shown above.

Explicitly Load Fixtures In Unit Tests for Legacy Classes

For legacy classes, fixtures must be loaded using Fixture.create_fixtures, can’t use the standard fixture: some_fixture


class WebTest < Test::Unit::TestCase
#  fixtures :webs, :lists	# Can't use this when using multiple databases

 	def setup
		ActiveRecord::Base.connection = LegacyBase.connection
		Fixtures.create_fixtures(File.dirname(__FILE__) + "/../fixtures", "webs"){LegacyBase.connection}		
		
		@web = Web.find("abc")
 	end
 
  
  # Replace this with your real tests.
  def test_truth
    assert_kind_of Web, @web

  end
end

Note I found that the ActiveRecord::Base.connection attribute had to be set otherwise when the test fixtures are inserted during the unit test runs, the column names in the SQL were being formatted with the default test database adapter’s formatting. In my case the mysql_adapter was adding “’” around each column name in the insert statement causing an invalid sql error when trying to insert into the legacy MS SQLServer database.

Explicitly Load Fixtures In Unit Tests for Regular Classes

Unit testing for model classes that are not derived from the LegacyBase class and connect to the default test database require the connection to be explicitly established and the fixtures loaded in a similar fashion to the legecy unit test classes. If the connection is not explicitly specified the loading of fixture data may use the connection specified in the legacy unit tests.


  def setup
  	ActiveRecord::Base.establish_connection("#{RAILS_ENV}")
  	Fixtures.create_fixtures(File.dirname(__FILE__) + "/../fixtures", "scrum_foos")
  end

Note The connection does not need to be specified for the create_fixtures call

Thanks to everyone who contributed to the multiple database topics on the wiki HowtoUseMultipleDatabases, couldn’t have figured this out without all of the good information.

- JSzod


I followed the previous solutions, but couldn’t figure out how to clone the structure from “legacy_development” to “legacy_test”.

I came up with a solution to use your legacy databases with your main test database. It has the benefit that fixtures will work as usual, but will break if you have tables with the same name in different lagacy databases. You need legacy.rake and base.rb below. Use at your own risk! I’m a newbie just hacking away. :)

Run with: rake db:test:prepare:legacy

# lib/tasks/legacy.rake
# 
# - prepare all legacy databases with rake db:test:prepare:legacy
# - db:test:*:legacy_db to run any db:test: task on database legacy_db
# 
# list your legacy dbs here:
legacy_dbs = %w(ciministry)

def prepend(prefix, array)
  array.collect { |item| prefix + item }
end

namespace :db do
  namespace :test do    
    namespace :prepare do

      desc "like db:test:prepare plus copies all legacy database structure to the test database" 
      task :legacy => "db:test:prepare" do
        prepend("db:test:clone_structure:", legacy_dbs).each do |task|
          Rake::Task[task].invoke
        end
      end
      
    end
    commands = %w(clone clone_structure)
    commands.each do |command|
      legacy_dbs.each do |legacy_db|
        namespace command do
        
          desc "like db:test:#{command} but for #{legacy_db}" 
          task legacy_db do
            config_path = "config/environments" 

            # db:test:clone will complain without this environments file
            File.copy("#{config_path}/#{RAILS_ENV}.rb", "#{config_path}/#{legacy_db}.rb")

            # fake the environment to get the legacy database's structure in the test db
            backup_env = RAILS_ENV;
            RAILS_ENV = legacy_db
            Rake::Task["db:test:#{command}"].invoke
            
            # restore
            File.delete("#{config_path}/#{legacy_db}.rb")
            RAILS_ENV = backup_env;
          end

        end
      end
    end
  end
end

Use this as your abstract base instead of the one presented above

class LegacyBase < ActiveRecord::Base
  self.abstract_class = true
  
  if RAILS_ENV == "test"
    establish_connection("test")
  else
    establish_connection("legacydb")
  end
end


- AndrewRoth