Tuesday, November 13, 2007

Ruby: Time::is

Mocha is fantastic for unit testing, but I usually try to avoid requiring it while functional testing. In general this works, but Time.now is something that I occasionally like to fix even while functional testing. To solve the problem, within a functional test helper I load a time_extensions.rb file that defines a Time::is method. The Time::is method is useful for freezing time at a certain point and executing a block of code. When the block of code finishes the Time.now method is returned to it's original implementation.

The example below is how I usually solve the described problem.

require 'time'

class Time
def self.metaclass
class << self; self; end
end

def self.is(point_in_time)
new_time = case point_in_time
when String then Time.parse(point_in_time)
when Time then point_in_time
else raise ArgumentError.new("argument should be a string or time instance")
end
class << self
alias old_now now
end
metaclass.class_eval do
define_method :now do
new_time
end
end
yield
class << self
alias now old_now
undef old_now
end
end
end

Time.is(Time.now) do
Time.now # => Tue Nov 13 19:31:46 -0500 2007
sleep 2
Time.now # => Tue Nov 13 19:31:46 -0500 2007
end

Time.is("10/05/2006") do
Time.now # => Thu Oct 05 00:00:00 -0400 2006
sleep 2
Time.now # => Thu Oct 05 00:00:00 -0400 2006
end

Rails: Enumerable#sum

Documentation
Calculates a sum from the elements. Examples:

payments.sum { |p| p.price * p.tax_rate }
payments.sum(&:price)

This is instead of payments.inject { |sum, p| sum + p.price }

Also calculates sums without the use of a block:

[5, 15, 10].sum # => 30

The default identity (sum of an empty list) is zero. However, you can override this default:

[].sum(Payment.new(0)) { |i| i.amount } # => Payment.new(0)
Usage
The Enumerable#sum method does exactly what you would expect: Sum the elements of the array.

Test

require 'rubygems'
require 'active_support'
require 'test/unit'
require 'dust'

unit_tests do
test "sum the numbers from the array" do
grades = [50, 55, 67, 62, 71, 89, 84, 85, 99]
assert_equal 662, grades.sum
end
end

Monday, November 12, 2007

Rails: Enumerable#group_by

Documentation
Collect an enumerable into sets, grouped by the result of a block. Useful, for example, for grouping records by date.

e.g.
  latest_transcripts.group_by(&:day).each do |day, transcripts|
p "#{day} -> #{transcripts.map(&:class) * ', '}"
end
"2006-03-01 -> Transcript"
"2006-02-28 -> Transcript"
"2006-02-27 -> Transcript, Transcript"
"2006-02-26 -> Transcript, Transcript"
"2006-02-25 -> Transcript"
"2006-02-24 -> Transcript, Transcript"
"2006-02-23 -> Transcript"
Usage
The Enumerable#group_by method is helpful for grouping elements of an Enumerable by an attribute or an arbitrary grouping. The documentation provides a good example of how to group by an attribute; however, the group_by method can be used logically group by anything returned from the block given to group_by.

Test

require 'rubygems'
require 'active_support'
require 'test/unit'
require 'dust'

unit_tests do
test "group by grades" do
grades = [50, 55, 60, 62, 71, 83, 84, 85, 99]
expected = {"A"=>[99], "B"=>[83, 84, 85], "C"=>[71], "D"=>[60, 62], "F"=>[50, 55]}
actual = grades.group_by do |grade|
case
when grade < 60 then "F"
when grade < 70 then "D"
when grade < 80 then "C"
when grade < 90 then "B"
else "A"
end
end
assert_equal expected, actual
end
end

Ruby: Testing Private Methods

Jason Rudolph recently wrote about Testing Private Methods in Ruby. Before I continue, it's probably a good idea to note that I rarely test private methods. I prefer to test through the public API. However, there are times when life is easier if you write a few tests for a private method or two.

The solution highlighted by Jason (original implementation from Evan Phoenix) is nice from a maintainability perspective since it encapsulates changing the behavior of the class within the test. However, it's a bit verbose for my taste.

Reading the entry I couldn't help but think that gaining temporary access to a private method for testing seems like something that can be handled by a general solution instead.

Here's how I would have solved the problem.

require 'rubygems'
require 'dust'
require 'test/unit'

class Ninja
private
def kill(num_victims)
"#{num_victims} victims are no longer with us."
end
end

class Class
def publicize_methods
saved_private_instance_methods = self.private_instance_methods
self.class_eval { public *saved_private_instance_methods }
yield
self.class_eval { private *saved_private_instance_methods }
end
end

unit_tests do
test "kill returns a murder string" do
Ninja.publicize_methods do
assert_equal '3 victims are no longer with us.', Ninja.new.kill(3)
end
end
end

Sunday, November 11, 2007

Rails: Enumerable#index_by

Documentation
Convert an enumerable to a hash. Examples:

people.index_by(&:login)
=> { "nextangle" => , "chade-" => , ...}
people.index_by { |person| "#{person.first_name} #{person.last_name}" }
=> { "Chade- Fowlersburg-e" => , "David Heinemeier Hansson" => , ...}
Usage
I've used Enumerable#index_by for 2 different reasons recently. In one instance I needed faster access to find an element in an array. We found that pulling something out of a hash was faster than using the find method of array. We did a few benchmarks similar to the contrived examples below.
array = [1..100].to_a
hash = array.index_by { |element| element }
array_bm = Benchmark.measure do
array.find { |element| element = 99 }
end
hash_bm = Benchmark.measure do
hash.include? 99
end

p array_bm.real # => 1.382
p hash_bm.real # => 1.001
note: we couldn't use the include? method for our particular instance, so I left it out of the contrived example.

The benchmarks for our project revealed even larger gains, thus it made sense to convert our array to a hash and work with that instead of the array.

The other usage we found for index_by was to utilize the fact that index_by overwrites the value instead of appending (which group_by does). We had a list of scores and wanted to group them together. The group_by method could handle that case; however, we only wanted to keep track of the highest score per group. The test below shows an example where a list of grades can be grouped by grade letter and the highest score per grade letter.

Test

require 'rubygems'
require 'active_support'
require 'test/unit'
require 'dust'

unit_tests do
test "index by highest numeric per grade level" do
grades = [50, 55, 67, 62, 71, 89, 84, 85, 99]
expected = {"A"=>99, "B"=>89, "C"=>71, "D"=>67, "F"=>55}
actual = grades.sort.index_by do |grade|
case
when grade < 60 then "F"
when grade < 70 then "D"
when grade < 80 then "C"
when grade < 90 then "B"
else "A"
end
end
assert_equal expected, actual
end
end