Showing posts with label ActiveRecord. Show all posts
Showing posts with label ActiveRecord. Show all posts

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

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.