UPDATE: A more comprehensive method of comparing arrays is covered in this updated article.

When you don’t care what order items in an array are in, don’t pretend you do.

Say you’ve got a list of expected results, and some rows returned by the database:

expected_words = [ 'some', 'known', 'items' ]
found_words = Words.find( :all, :conditions => [ 'name IN (?)', expected_words ] ).map { |w| w.name }

Instead of asserting:

assert_equal expected_words, found_words

Consider:

assert_same_elements expected_words, found_words

To achieve this, I follow the pattern presented by Jay Fields in Convention Is Important, extending a core object and placing the extension in central location. In my case, this location is lib/core_extensions/array/same_elements.rb

class Array
  # Ask an Array whether it shares the same elements with another Array, irrespective of order
  # Options
  # :allow_duplicates
  #   If set to true arrays with the same elements, but differing numbers of those elements
  #   are treated as the same.
  #   Examples
  #     [ :a ].same_elements?( [ :a, :a ] ) => false
  #     [ :a ].same_elements?( [ :a, :a ], :allow_duplicates => true) => true
  def same_elements? another_array, options = {}
    raise ArgumentError, "#{another_array.inspect} was expected to be an Array" unless another_array.kind_of?(Array)
    s = self
    a = another_array
    if options[:allow_duplicates]
      s = s.uniq
      a = a.uniq
    end
 
    return element_counts(s) == element_counts(a)
  end
 
  private
    def element_counts obj
      result = []
      obj.uniq.map { |e|
        [ e, obj.inject(0) { |i, e2| i + (e == e2 ? 1 : 0 ) } ]
      }.each { |p| result << p.first; result << p.last }
      Hash[ *result ]
    end
end

This extensions allows us to ask an array about its similarity to another array.

The most interesting part of this method is the element_counts, which returns a hash whose keys are the unique elements of the array and whose values are the number of instances of each unique value in the original array.

In order to be able to use this in your tests, simply add this small helper method to test/test_helper.rb

# Assert two enumerables have the same elements, irrespective of order
def assert_same_elements enum1, enum2, *args
  message = args.last.kind_of?(String) ? args.pop : "Expected Arrays to have same elements"
  options = args.last.kind_of?(Hash) ? args.pop : {}
  assert_block(build_message(message, "<?> expected to have the same elements as \n<?>.\n", enum2, enum1)) { enum1.same_elements?(enum2, options) }
end

Bonus: Tests

require File.dirname(__FILE__) + '/../unit_test_helper'
 
class ArraySameElementsTest < Test::Unit::TestCase
  def test_same_elements_with_identical_arrays
    assert_equal true, [ :a ].same_elements?( [ :a ] )
  end
 
  def test_same_elements_with_no_shared_items
    assert_equal false, [ :a ].same_elements?( [ :b ] )
  end
 
  def test_same_elements_with_different_ordering
    assert_equal true, [ :a, :b ].same_elements?( [ :b, :a ] )
  end
 
  def test_same_elements_should_not_allow_duplicates
    assert_equal false, [ :a ].same_elements?( [ :a, :a ] ), 'Should not allow duplicates in other array.'
    assert_equal false, [ :a, :a ].same_elements?( [ :a ] ), 'Should not allow duplicates in self.'
    assert_equal false, [ :a, :b, :b ].same_elements?( [ :a, :a, :b ] ), 'Should not allow different duplicates in self and other.'
    assert_equal true, [ :a, :b, :b ].same_elements?( [ :a, :b, :b ] ), 'Should allow duplicates present in both self and other.'
  end
 
  def test_same_elements_should_allow_duplicates_if_allow_duplicates
    assert_equal true, [ :a ].same_elements?( [ :a, :a ], :allow_duplicates => true )
    assert_equal true, [ :a, :a ].same_elements?( [ :a ], :allow_duplicates => true )
    assert_equal true, [ :a, :b, :b ].same_elements?( [ :a, :a, :b ], :allow_duplicates => true )
  end
 
  def test_same_elements_should_not_allow_different_elements_if_allow_duplicates
    assert_equal false, [ :a, :b ].same_elements?( [ :a, :a, :b, :c ], :allow_duplicates => true )
  end
 
  def test_same_elements_should_raise_argument_error_if_provided_argument_is_not_an_array
    assert_raise_with_error_message ArgumentError, :error_message => / was expected to be an Array$/ do
      [ ].same_elements? nil
    end
  end
end

Sometimes I want to verify the value within a constant is handled in a certain way, independently of what the actual value in the constant is defined as.

So I change it, temporarily.

ApplicationConfig.override_constant(:RECORD_ANALYTICS, false) do
  puts "For the duration of this block, RECORD_ANALYTICS is #{ApplicationConfig::RECORD_ANALYTICS}"
end

# Override the value of a Module Constant for the duration of a block.
class Module
  def override_constant constant_name, new_value
    raise ArgumentError, 'No block provided to establish a context in which the constant is overridden' unless block_given?
    # Cannot override unless constant is defined in Module
    raise NameError, "#{self} does not define #{constant_name}" unless const_defined?(constant_name)
 
    # Store original value to replace it at the end
    original_value = const_get(constant_name)
 
    # Remove original definition
    remove_const constant_name
 
    # Insert alternative definition
    const_set constant_name, new_value
 
    begin
      yield
 
    ensure
      # Remove alternative definition
      remove_const constant_name
 
      # Restore original definition
      const_set constant_name, original_value
    end
  end
end

class OverrideConstantTest < Test::Unit::TestCase
  module TestModule
    DEFINED_CONSTANT = true
  end
 
  def test_should_raise_argument_error_when_block_is_not_provided
    assert_raise_with_error_message ArgumentError, :error_message => 'No block provided to establish a context in which the constant is overridden' do
      TestModule.override_constant :DEFINED_CONSTANT, true
    end
  end
 
  def test_should_raise_error_when_module_does_not_define_constant
    assert_raise_with_error_message NameError, :error_message => 'OverrideConstantTest::TestModule does not define NO_SUCH_CONSTANT' do
      TestModule.override_constant :NO_SUCH_CONSTANT, true do
      end
    end
  end
 
  def test_should_alter_value_of_constant_for_duration_of_block
    assert_equal true, TestModule::DEFINED_CONSTANT
 
    TestModule.override_constant :DEFINED_CONSTANT, false do
      assert_equal false, TestModule::DEFINED_CONSTANT
    end
 
    assert_equal true, TestModule::DEFINED_CONSTANT
  end
 
end

Verifying errors are raised is handy, but are you sure you’re capturing the right one?

Check the message!

# assert_raise allows you to check for the type of an error but not to check the error messages contents
# This is a wrapper to assert_raise that adds this functionality.
#
# Use with the traditional assert_raise syntax
# http://www.ruby-doc.org/stdlib/libdoc/test/unit/rdoc/classes/Test/Unit/Assertions.html
#
# To specify the error message, provide a string or regular expression to the :error_message option
#
# Example
# assert_raise_with_error_message ArgumentError, :error_message => /is not refuelable$/ do
#   outfitter.refuel(Asteriod.new)
# end
#
class Test::Unit::TestCase
  def assert_raise_with_error_message *args, &block
    options = args.last.kind_of?(Hash) ? args.pop : {}
    exception = assert_raise *args, &block
    expected_message = options[:error_message]
    case expected_message
    when String
      assert_equal expected_message, exception.message
    when Regexp
      assert_match expected_message, exception.message
    end
  end
end

# Validations based on negations of regular expressions.
# validates_not_format_of takes an array of attributes,
#  a regular expression to apply to them,
#  and an optional hash of configuration options.
#
# All other validations take an array of attributes,
#  a word or list of words to act upon,
#  and an optional hash of configuration options.
#
# Examples:
# validates_does_have_the_words :username, :description, %w(chicken yellow coward), :ignore_case => true
# In this case, the model will not be valid if the username or description attributes contain any of the
# restricted words, regardless of case.
#
module ActiveRecord
  module Validations
    module ClassMethods
 
      # Negates a regular expression match against an attribute.
      # Similar to grep -v
      #
      def validates_not_format_of(*attr_names)
        configuration = { :message => ActiveRecord::Errors.default_error_messages[:invalid], :on => :save, :with => nil }
        configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)
 
        raise(ArgumentError, "A regular expression must be supplied as the :with option of the configuration hash") unless configuration[:with].is_a?(Regexp)
 
        validates_each(attr_names, configuration) do |record, attr_name, value|
          record.errors.add(attr_name, configuration[:message]) if value.to_s =~ configuration[:with]
        end
      end
 
      def validates_does_not_begin_with_the_word(*attr_names)
        validates_attributes_not_format_of_block(attr_names) { |word| "^#{word}" }
      end
      alias :validates_does_not_begin_with_the_words :validates_does_not_begin_with_the_word
 
      # Validates an attribute does not end with the provided string, or strings.
      # Examples:
      # validates_does_not_end_with_the_word :username, '2000'
      #  or
      # validates_does_not_end_with_the_words :username, %w(xxx ooo 666 McFly)
      #
      def validates_does_not_end_with_the_word(*attr_names)
        validates_attributes_not_format_of_block(attr_names) { |word| "#{word}$" }
      end
      alias :validates_does_not_end_with_the_words :validates_does_not_end_with_the_word
 
      # Validates an attribute does not contain disallowed words
      # Examples:
      # validates_does_not_have_the_word :username, 'admin'
      #  or
      # validates_does_not_have_the_words :username, %w(admin guest moderator)
      #
      def validates_does_not_have_the_word(*attr_names)
        validates_attributes_not_format_of_block(attr_names) { |word| word }
      end
      alias :validates_does_not_have_the_words :validates_does_not_have_the_word
 
      def validates_is_not_the_word(*attr_names)
        validates_attributes_not_format_of_block(attr_names) { |word| "^#{word}$" }
      end
      alias :validates_is_not_the_words :validates_is_not_the_word
 
      private
      # The block supplied to validates_attributes_not_format_of_block must yield a string
      # that can be converted into a regular expression by Regexp.new
      #
      # The argument supplied to the block is a string that has been regular-expression-quoted,
      # and is safe to use without further escaping.
      #
      # You can pass options for how the regular expression is created in the final options hash.
      # Either use :ignore_case => true
      #  or
      # :regexp_options => Fixnum
      # either of which will be passed as the 2nd argument to Regexp.new, with :regexp_options taking precedence
      # See documentation on Regexp.new for more information on allowable values for :regexp_options
      #
      def validates_attributes_not_format_of_block(attr_names, &block)
        options = attr_names.last.is_a?(Hash) ? attr_names.pop : { }
        word = attr_names.pop
 
        words = word.respond_to?(:each) ? word : [ word ]
        regexp_options = options.delete(:regexp_options) || options.delete(:ignore_case)
 
        restriction = Regexp.new(words.map { |w| "(#{yield Regexp.quote(w.to_s)})" }.join('|'), regexp_options)
 
        configuration = {
          # Overridable default configuration
          :message => 'is not valid',
          :on => :save
        }.merge(options || {}).merge(
          # Mandatory default configuration
          :with => restriction
        )
        validates_not_format_of attr_names, configuration
      end
    end
  end
end

def authorized_only &block
  @active_user ||= User.find_by_id( session[:user_id] )
  if @active_user
    yield block if block_given?
  elsif !block_given?
    flash[:notice] = "Please log in"
    redirect_to(:controller => "login", :action => "login")
  end
end

module ActiveRecord
  class Base
    # Step through and instantiate each member of the class and execute on it,
    #   but instantiate no more than per_page instances at any given time.
    # Safe for destructive actions or actions that modify the fields
    # your :order or :conditions clauses operate on.
    def each_by_page per_page, options = {}, &block
      # By-id for model-modifying blocks
      # Build SQL to get ids of all matching records using the options provided by the user
      sql = construct_finder_sql(options.dup.merge({ :select => "`#{table_name}`.#{primary_key} AS id" }))
      # Get the results as an array of tiny hashes { "id" => "1" } and flatten them out to just the ids
      all_ids = connection.select_all(sql).map { |h| h['id'] }
      at_a_time = 0..(per_page-1)
 
      # chop apart the all_ids array a segment at a time
      begin
        ids = all_ids.slice!(at_a_time)
        ids_cases = []
        ids.each_with_index { |id, i| ids_cases << "WHEN #{id} THEN #{i}" }
        ids_cases = ids_cases.join(' ')
 
        # Do the deed on this page of results
        find(:all, options.merge(
          :conditions => [ "#{primary_key} IN (?)", ids ],
          :order => "CASE id #{ids_cases} END"
        )).each &block
 
      end until all_ids.empty?
    end
  end
end

This piece of code is perhaps named poorly. It arose from a need to operate on a large number of models during a migration.

Many migrations that operate on a large number of rows can accomplish their operation en-masse using an update_all.

User.update_all('filename = \'default.png\'', 'default_avatar = true')

However, sometimes the operation being performed is more complex. Perhaps its a destructive migration. Perhaps it operates on a complex graph of objects, or over a polymorphic association, or perhaps you’re relying on the propagation of ActiveRecord’s callbacks.

In any of these cases, you can easily exhaust available RAM with code such as this:

User.find(:all, :conditions => { :default_avatar => true } ) do |user| user.select_default_avatar end

ActiveRecord will first do a SQL SELECT with the provided parameters, instantiate each row of the result as an object in memory, and once all rows are instantiated, will iterate over the set, applying the block to each element.

Except if the number of rows returned is large enough, it never gets to the iterating part.

Instead of instantiating all the models at once, we instead instantiate them in bite-sized pieces.

User.each_by_page(:conditions => { :default_avatar => true } ) do |user| user.select_default_avatar end