Back to Repositories

Testing ActiveRecord Database Integration in will_paginate

This test connector provides a robust testing infrastructure for ActiveRecord integration within the will_paginate library. It establishes database connections, loads test schemas, and manages fixtures for comprehensive database testing scenarios.

Test Coverage Overview

The test suite provides comprehensive coverage for database connectivity and fixture management:

  • Database connection setup and configuration validation
  • Schema loading and fixture handling
  • Query monitoring and SQL tracking functionality
  • Support for multiple database adapters

Implementation Analysis

The testing approach utilizes a modular connector design that abstracts database setup complexities. It implements ActiveRecord notifications for query tracking, handles timezone configurations, and provides flexible fixture loading through the FixtureSetup module.

Key patterns include dependency autoloading, database configuration management, and fixture caching for optimized test performance.

Technical Details

  • ActiveRecord and ActiveSupport integration
  • ERB template processing for database configurations
  • YAML configuration management
  • StringIO for captured output
  • Custom fixture loading mechanisms
  • Database schema management

Best Practices Demonstrated

The test connector exemplifies several testing best practices:

  • Environment-aware configuration handling
  • Efficient fixture caching and lazy loading
  • Clean separation of setup and execution concerns
  • Robust error handling and reporting
  • Modular and maintainable code structure

mislav/will_paginate

spec/finders/activerecord_test_connector.rb

            
require 'active_record'
require 'active_record/fixtures'
require 'stringio'
require 'erb'
require 'time'
require 'date'
require 'yaml'

# forward compatibility with Rails 7 (needed for time expressions within fixtures)
class Time
  alias_method :to_fs, :to_s
end unless Time.new.respond_to?(:to_fs)

# monkeypatch needed for Ruby 3.1 & Rails 6.0
YAML.module_eval do
  class << self
    alias_method :_load_orig, :load
    def load(yaml_str)
      _load_orig(yaml_str, permitted_classes: [Symbol, Date, Time])
    end
  end
end if YAML.method(:load).parameters.include?([:key, :permitted_classes])

$query_count = 0
$query_sql = []

ignore_sql = /
    ^(
      PRAGMA | SHOW\ (max_identifier_length|search_path) |
      SELECT\ (currval|CAST|@@IDENTITY|@@ROWCOUNT) |
      SHOW\ ((FULL\ )?FIELDS|TABLES)
    )\b |
    \bFROM\ (sqlite_master|pg_tables|pg_attribute)\b
  /x

ActiveSupport::Notifications.subscribe(/^sql\./) do |*args|
  payload = args.last
  unless payload[:name] =~ /^Fixture/ or payload[:sql] =~ ignore_sql
    $query_count += 1
    $query_sql << payload[:sql]
  end
end

module ActiverecordTestConnector
  extend self
  
  attr_accessor :connected

  FIXTURES_PATH = File.expand_path('../../fixtures', __FILE__)

  # Set our defaults
  self.connected = false

  def setup
    unless self.connected
      setup_connection
      load_schema
      add_load_path FIXTURES_PATH
      self.connected = true
    end
  end

  private

  module Autoloader
    def const_missing(name)
      super
    rescue NameError
      file = File.join(FIXTURES_PATH, name.to_s.underscore)
      if File.exist?("#{file}.rb")
        require file
        const_get(name)
      else
        raise $!
      end
    end
  end
  
  def add_load_path(path)
    if ActiveSupport::Dependencies.respond_to?(:autoloader=)
      Object.singleton_class.include Autoloader
    else
      dep = defined?(ActiveSupport::Dependencies) ? ActiveSupport::Dependencies : ::Dependencies
      dep.autoload_paths.unshift path
    end
  end

  def setup_connection
    db = ENV['DB'].blank?? 'sqlite3' : ENV['DB']

    erb = ERB.new(File.read(File.expand_path('../../database.yml', __FILE__)))
    configurations = YAML.load(erb.result)
    raise "no configuration for '#{db}'" unless configurations.key? db
    configuration = configurations[db]
    
    # ActiveRecord::Base.logger = Logger.new(STDOUT) if $0 == 'irb'
    puts "using #{configuration['adapter']} adapter"
    
    ActiveRecord::Base.configurations = { db => configuration }
    ActiveRecord::Base.establish_connection(db.to_sym)
    if ActiveRecord.respond_to?(:default_timezone=)
      ActiveRecord.default_timezone = :utc
    else
      ActiveRecord::Base.default_timezone = :utc
    end
  end

  def load_schema
    begin
      $stdout = StringIO.new
      ActiveRecord::Migration.verbose = false
      load File.join(FIXTURES_PATH, 'schema.rb')
    ensure
      $stdout = STDOUT
    end
  end

  module FixtureSetup
    def fixtures(*tables)
      table_names = tables.map { |t| t.to_s }

      fixtures = ActiveRecord::FixtureSet.create_fixtures(ActiverecordTestConnector::FIXTURES_PATH, table_names)
      @@loaded_fixtures = {}
      @@fixture_cache = {}

      unless fixtures.nil?
        fixtures.each { |f| @@loaded_fixtures[f.table_name] = f }
      end

      table_names.each do |table_name|
        define_method(table_name) do |*names|
          @@fixture_cache[table_name] ||= {}

          instances = names.map do |name|
            if @@loaded_fixtures[table_name][name.to_s]
              @@fixture_cache[table_name][name] ||= @@loaded_fixtures[table_name][name.to_s].find
            else
              raise StandardError, "No fixture with name '#{name}' found for table '#{table_name}'"
            end
          end

          instances.size == 1 ? instances.first : instances
        end
      end
    end
  end
end