Category Archives: Rails

Using Factories for Rails Fixtures and Test Doubles

Chris Wanstrath has written about making Rails fixtures less painful than they need to be with the FixtureScenarios plugin. Personally, I prefer the Factory approach, nicely explained by Daniel Manges.

I’ve been using factory methods to create in-database ActiveRecord objects for a project that I’ve been working on in Bezurk. Reading Daniel’s article gave me a few ideas on improving the way I create fixtures and mocks. Since I’ve been using RSpec extensively in this project, I’ll present the examples in RSpec.

As the models evolve with the design and its behaviour change accordingly, there is a need to go through all the specifications that create this model and make sure that its created in a valid state. This is more pronounced with the use of test doubles, the test doubles also need to have its method stubs changed to reflect the latest state of the model that its is representing. I happen to make much use of test doubles for test isolation, so trying to manage all these objects became an exercise in patience. As it was getting painful, It’s time to change the way I create these models and test doubles.

As always, a layer of indirection will always go some way to solving a software problem. We introduce a Factory that encapsulates the creation of ActiveRecord objects by providing creation methods.

[ruby]
module FixtureFactory
def create_user(attributes = {})
User.create!(ModelAttributes.user(attributes))
end
end
[/ruby]
We’ll have a Factory for test doubles too.

[ruby]
module MockFactory
def mock_user(method_stubs = {})
mock_model(User, ModelAttributes.user(method_stubs))
end
end
[/ruby]
And the attributes for this model will be declared in a module that’s used by both Factories

[ruby]
module ModelAttributes
def self.user(attributes)
attributes.reverse_merge({:name => ‘doug’})
end
end
[/ruby]

The Factory modules are then included in Spec::Runnner

[ruby]
Spec::Runner.configure do |config|
include FixtureFactory
include MockFactory
end
[/ruby]

The objects can now be created using the factory methods available to all specifications.

[ruby]
doug = create_user
doppelganger = mock_user
[/ruby]

Update
Added links to Chris Wanstrath and Daniel Manges’ articles on managing Rails fixtures.

Have full disclosure for code vulnerabilities

The Rails core team released 1.1.6 of the framework today, a day after 1.1.5 was released. This was to fix a serious vulnerability in the Routes module. The core team has been extremely prompt in publicising the hole and in releasing fixes.

However(you know there had to be one), I take issue with how the first fix release (1.1.5) was handled. It appears that this release did not fully rectify the problem, hence the need for 1.1.6. While DHH revealed the reasons for 1.1.5, he did not detail exactly what was wrong, opting for a security through obscurity approach.

In retrospect, a full disclosure policy would have been a better move. This would have given developers more information in deciding whether to shut down their sites, in view of the implications(data loss/theft et al) of having it compromised.

That said, if you’re running a rails web application in the wild, UPGRADE NOW.

EDIT: mixed up my rails versions, doh!

Automatically creating files for file_column models in Fixtures

I wanted to be able to have my testing regime automatically create files associated with model fixtures defined in the fixture files. Coincidentally, I’m using Sebastien Kanthak’s file_column plugin for managing files in models.

After looking at the Fixture class and the FileColumn module, I realised that I needed a way to store the model attribute(s) that were passed in the call to file_column. What FileColumn did was create the methods according to the attribute passed into the method but the attribute itself is not actually stored anywhere.

Time to extend FileColumn:

[ruby]require File.join(RAILS_ROOT, ‘vendor’, ‘plugins’, ‘file_column’, ‘lib’, ‘file_column’)

module FileColumn
module ClassMethods
@@file_column_attributes = {}
alias :aliased_file_column :file_column
def file_column(attr, options = {})
aliased_file_column(attr, options)
klass = self.name.constantize
@@file_column_attributes[klass] ||= []
@@file_column_attributes[klass] << attr
end

def file_attributes
@@file_column_attributes[self.name.constantize]
end
end
end[/ruby]

As the name implies, the methods in ClassMethods are class methods on the including class. So in order to store the file_column attributes, we need a class variable, @@file_column_attributes. This variable is a hash with the model class name as the key and an array containing the attributes.I'll also implement a class method so that I'll be able to do Model.file_attributes and know what attributes have been file_columnised.

Next, we'll need to change the way fixtures are populated to the test database. This involves extending the Fixtures class.

We'll do this in test_helper.rb

[ruby]
require 'fileutils'
class Fixtures
SAMPLE_FILE = File.join(Test::Unit::TestCase.fixture_path, 'files', 'sample.pdf')
include FileUtils

alias :original_insert_fixtures :insert_fixtures

def insert_fixtures
original_insert_fixtures
create_files(@class_name, values) unless @class_name.constantize.file_attributes.nil?
rescue NameError
# workaround for HABTM fixtures
end

def create_files(klass, values)
model_dir = File.join(Test::Unit::TestCase.fixture_path, 'media', 'uploads', klass.downcase)
values.each do |fixture|
klass.constantize.file_attributes.each do |attr|
create_file(fixture, attr, model_dir) unless attr.nil?
end
end
def create_file(fixture, attribute, parent_dir)
attribute = attr.to_s
dest_dir = mkpath(File.join(parent_dir, attribute, fixture['id'].to_s))
file_dest = File.join(dest_dir, fixture[attribute])
cp(SAMPLE_FILE, file_dest) unless fixture[attribute].nil? or File.exists?(file_dest)
end
end
[/ruby]

The extended insert_fixtures method first invokes original_insert_fixtures, then checks whether the model has any file_column attributes. If it does, the files defined in the fixtures will be created if they don't already exist.

EDIT: Running functional tests with rake test:functionals fails miserably even though running the tests individually is just fine.