Saturday, March 24, 2007

Rails: ActiveRecord Unit Testing part II

Back in December I wrote about unit testing ActiveRecord models. Following that post, we started using that pattern to unit test our models. It quickly became obvious that the ActiveRecord::ConnectionAdapters::Column.new("digits", nil, "string", false) statements were largely static and we wrapped this behavior in a fluent interface.

This worked, but some developers on the team didn't like the (arguably) unnecessary additional syntax. These developers argued that since none of our Unit Tests hit the database we should simply override the new method and handle stubbing the columns method.

You can argue that adding magic to the new method could be confusing, but our team has the standard that no unit test can hit the database. Therefore, everyone is very aware that, despite the standard syntax, we are injecting our own column handling mechanism.

The code to make this work is fairly straightforward. The usual alias method gotchas apply so I chose to use an alternative. Adding the following code will allow you to create disconnected models in your unit tests simply by calling new.
class << ActiveRecord::Base

new_method = instance_method :initialize
define_method :initialize do |*attributes|
attributes = attributes.empty? ? nil : attributes.first
self.stubs(:columns).returns(no_db_columns(attributes))
new_method.bind(self).call attributes
end

def no_db_columns attributes
return [] if attributes.nil?
attributes.keys.collect do |attribute|
sql_type = case attributes[attribute].class
when " " then "integer"
when "Float" then "float"
when "Time" then "time"
when "Date" then "date"
when "String" then "string"
when "Object" then "boolean"
end
ActiveRecord::ConnectionAdapters::Column.new(attribute.to_s, nil, sql_type, false)
end
end

end

Friday, March 23, 2007

Ruby: === operator

Recently I was looking at creating a patch for Mocha that would allow you to specify a class as an argument. The feature was added to allow you to specify that an argument can be any instance of the class specified.
object.expects(:do_this).with(Fixnum, true, 99)
object.do_this(2, true, 99) # satisfies the expectation
To support this new feature I looked into the === method.

The === method of Object is defined as:
Case Equality—For class Object, effectively the same as calling #==, but typically overridden by descendents to provide meaningful semantics in case statements.
There's no mystery here, use === like ==. However, the === method of Module provides the behavior I'm looking for.
Case Equality—Returns true if anObject is an instance of mod or one of mod‘s descendents. Of limited use for modules, but can be used in case statements to classify objects by class.
The documentation can be tested easily to ensure the behavior I'm looking for.
Fixnum === 2 #=> true
An important thing to note is that 2 and Fixnum are not commutative.
2 === Fixnum #=> false
So, using the === method I was able create the following patch that provides the behavior I was looking for.
Index: trunk/test/unit/expectation_test.rb
===================================================================
--- trunk/test/unit/expectation_test.rb (revision 109)
+++ trunk/test/unit/expectation_test.rb (working copy)
@@ -30,6 +30,16 @@
assert expectation.match?(:expected_method, 1, 2, 3)
end

+ def test_should_match_calls_to_same_method_with_expected_parameter_values_or_class
+ expectation = new_expectation.with(1, Fixnum, Object)
+ assert expectation.match?(:expected_method, 1, 2, 3)
+ end
+
+ def test_should_match_calls_to_same_method_with_expected_parameter_values_or_class
+ expectation = new_expectation.with(1, Fixnum, Object)
+ assert expectation.match?(:expected_method, 1, Fixnum, 3)
+ end
+
def test_should_match_calls_to_same_method_with_parameters_constrained_as_expected
expectation = new_expectation.with() {|x, y, z| x + y == z}
assert expectation.match?(:expected_method, 1, 2, 3)
Index: trunk/lib/mocha/expectation.rb
===================================================================
--- trunk/lib/mocha/expectation.rb (revision 109)
+++ trunk/lib/mocha/expectation.rb (working copy)
@@ -22,9 +22,6 @@
class InvalidExpectation < Exception; end

class AlwaysEqual
- def ==(other)
- true
- end
end

attr_reader :method_name, :backtrace
@@ -43,7 +40,15 @@
end
def match?(method_name, *arguments)
- (@method_name == method_name) and (@parameter_block ? @parameter_block.call(*arguments) : (@parameters == arguments))
+ return false unless @method_name == method_name
+ return @parameter_block.call(*arguments) unless @parameter_block.nil?
+ return true if @parameters.is_a? AlwaysEqual
+ return false unless @parameters.size == arguments.size
+ @parameters.inject([0, true]) do |result, arg|
+ result[1] &&= (arguments[result.first].is_a?(Module) ? arg == arguments[result.first] : arg === arguments[result.first])
+ result[0] += 1
+ result
+ end.last
end
# :startdoc:

Should Rails include Mocha?

I like the way the Rails integration tests currently depend on Mocha.
# integration_test.rb
...
begin # rescue LoadError
require 'mocha'
...
rescue LoadError
$stderr.puts "Skipping integration tests. `gem install mocha` and try again."
end
In fact, I stole the idea and changed it up a bit for sqldsl.
unless File.directory? File.dirname(__FILE__) + '/../vendor/mocha-0.4.0/'
raise "mocha 4.0 is required to run the test suite. create the 'vendor' directory as
a sibling of test and 'gem unpack mocha' in 'vendor'"
end
$:.unshift File.dirname(__FILE__) + '/../vendor/mocha-0.4.0/lib/'
require File.dirname(__FILE__) + '/../vendor/mocha-0.4.0/lib/mocha'
However, now I'm wondering if Rails should depend on, and include Mocha.

I've previously written that I think xUnit frameworks should include Mocks. Since Rails bakes in testing, I believe it should provide you with the full array of testing tools, including Mocks.

Thoughts?

Thursday, March 22, 2007

Rails: ActiveRecord Serialize method

On my current project we have a model with a few attributes that are instances. Generally, this is handled with a relationship (e.g. belongs_to). However, these attributes are not ActiveRecord::Base subclass instances. ActiveRecord handles this situation by providing the ActiveRecord::Base.serialize class method.

As a contrived example, imagine a UserAccount class that has a AuthorizationConfirmation instance as an attribute.
class CreateModels < ActiveRecord::Migration
def self.up
create_table :user_accounts do |t|
t.column :authorization_confirmation, :string
end
end

def self.down
drop_table :user_accounts
end
end

class UserAccount < ActiveRecord::Base
serialize :authorization_confirmation, AuthorizationConfirmation
end

class AuthorizationConfirmation
attr_accessor :fingerprint, :key
end
As you can see in the above example, the UserAccount class has the attribute authorization_confirmation. The value stored in authorization_confirmation is expected to be an instance of the AuthorizationConfirmation class.

A simple test proves the expected behavior.
require File.expand_path(File.dirname(__FILE__) + "/../test_helper")

class UserAccountTest < Test::Unit::TestCase
def test_authorization_confirmation_is_serialized_and_deserialized
account = UserAccount.new
account.authorization_confirmation = AuthorizationConfirmation.new
account.authorization_confirmation.fingerprint = "Xc1sseE"
account.authorization_confirmation.key = "gEteEQ"
account.save
retrieved = UserAccount.find account.id
assert_equal "Xc1sseE", retrieved.authorization_confirmation.fingerprint
assert_equal "gEteEQ", retrieved.authorization_confirmation.key
end
end
The test proves that the instance of AuthorizationConfirmation is saved and retrieved as expected.

A quick peek in the database shows the instance as yaml.
 id | authorization_confirmation                           
----+------+-------------------------------------------------------------------------------
6 | --- !ruby/object:AuthorizationConfirmation
fingerprint: Xc1sseE
key: gEteEQ
NOTE: At the time of this writing any attribute that is serialized cannot be nil. Ticket 7293 should resolve this problem.

Saturday, March 17, 2007

Ruby: sqldsl 1.4.0 released

This morning I released the 1.4.0 version of sqldsl. This release is mostly in response to the suggestions that sqldsl has recently gotten from the Ruby Community. The new version can be retrieved via "gem install sqldsl" as soon as the file propagates. For now, it can be downloaded from the download page.

Breaking Changes:
Table and column aliasing is now done with an 'as' method instead of using hashes. This change was necessary since column order is important and hashes are unordered.
irb> Select[:column1.as(:book), 1.as(:total), 'foo'.as(:constant)].to_sql
=> "select column1 as book, 1 as total, 'foo' as constant"

irb> Select.all.from[:table1.as(:aliased_table_name)].to_sql
=> "select * from table1 aliased_table_name"
Equality in the where clause has been changed from a single equals to double equals. I personally preferred the single equals; however, it broke in scenarios where columns were not prefixed by table names. The previous solution did not work because ruby assigned a local variable instead of raising a method_missing and allowing the ReceiveAny class to handle the assignment.

The new syntax is the same, except it uses == instead of =.
Select[:column1].from[:table1].where do
column1 == 99
end.to_sql
=> "select column1 from table1 where column1 = 99"
New Feature:
Inner Joins are now also supported
Select.all.from[:t1.as(:a)].inner_join[:t2.as(:b)].on do
a.id == b.id
end.inner_join[:t3.as(:c)].on do
b.id2 == c.id
end.to_sql
=> "select * from t1 a inner join t2 b on a.id = b.id inner join t3 c on b.id2 = c.id"
Bug Fix:
Or conditions are surrounded by parenthesis.
Select[:column1].from[:table1].where do
column1.equal 0
end.or do
column1 > 100
end.to_sql
=> "select column1 from table1 where column1 = 0 or (column1 > 100)"