RailRoad III
Testing
You can't really follow the the Test Driven Development methodology without writing tests as you develop. While the ROR documentation is fairly complete to get started on building an application, the organization of the documentation (as well as most reference material) seems to put testing at a lower priority in the context of the learning the Rails platform. It would be nice if the documentation illustrated TDD within the examples as well.
Fixtures
Rails provides a facility for quickly creating reproducible testing data through YAML fixture files. The key point of using a fixture as of 2.2.2 is that it is essentially a fully fledged ActiveRecord. When you run a test against a feature, one of the first things that happens is that Rails populates the database table with the fixture information. It is important that your fixture YAML file is syntactically correct - if the data specified in the file references an undefined column or violates a database constraint, Rails will fail to run the test and generate a whack tonne of arcane error messages. Don't do this unless you like to read stack traces.
Strange Boolean Behavior. If validates_presence_of is placed on a boolean attribute, it can only be assigned true values within the fixture. Attempting to assign false to the fixture attribute will break unit tests with the following error:
Attribute_name can't be blank.is not true.
In addition, when assigning boolean values within tests, you cannot use the false keyword. Doing so will create an invalid record. The numeric convention to assign true or false is to use 0 and 1 respectively.
Validation of Date versus DateTime Objects. Fixtures can use eRB to embed ruby code within the fixture itself. Unfortunately, when it comes to Date / DateTime attributes, the object returned by the fixture record is database dependent. In most cases, to validate date/time attributes requires using the strftime comparison. There is no way to get a Date object from a DateTime object. Seems intuitive, but Rails does not provide this facility. For simplicity, I've included the format parameters for posterity:
%a - The abbreviated weekday name (``Sun'')
%A - The full weekday name (``Sunday'')
%b - The abbreviated month name (``Jan'')
%B - The full month name (``January'')
%c - The preferred local date and time representation
%d - Day of the month (01..31)
%H - Hour of the day, 24-hour clock (00..23)
%I - Hour of the day, 12-hour clock (01..12)
%j - Day of the year (001..366)
%m - Month of the year (01..12)
%M - Minute of the hour (00..59)
%p - Meridian indicator (``AM'' or ``PM'')
%S - Second of the minute (00..60)
%U - Week number of the current year,
starting with the first Sunday as the first
day of the first week (00..53)
%W - Week number of the current year,
starting with the first Monday as the first
day of the first week (00..53)
%w - Day of the week (Sunday is 0, 0..6)
%x - Preferred representation for the date alone, no time
%X - Preferred representation for the time alone, no date
%y - Year without a century (00..99)
%Y - Year with century
%Z - Time zone name
%% - Literal ``%'' character
t = Time.now
t.strftime("Printed on %m/%d/%Y")
#=> "Printed on 04/09/2003"
t.strftime("at %I:%M%p")
#=> "at 08:56AM"
Attempting to resolve the fixture issues highlights one of Rails glaring deficiencies - the lack of authoritative, comprehensive documentation. From IRC, to the blogs I researched to troubleshoot these issues, it was apparent that everyone had rather unsubstantiated opinions regarding built-in testing facilities on Rails which were largely incomplete. No application is perfect, but this is a rather obvious omission.
testhelper.rb
By default, the testhelper.rb file has the fixtures :all parameter enabled. If your scaffold YAML files are not valid, this setting will only throw a lot of unintelligible errors. To build up your test suite one model at a time, it might be useful to simply have a script with ruby commands to run tests you've already finished. For example, I have a file that I've chown to be executable which contains commands to run individual tests.
ruby unit/model1_test.rb ruby unit/model2_test.rb
The testhelper.rb file is not all bad. In fact when used in it's primary capacity, it's quite useful. For example, instead of writing custom assertions for my unit tests, I placed a bunch of these in testhelper.rb. This allows you to follow the DRY (Don't Repeat Yourself) principle an simplify test suite creation. Combined with TextExpander, it's positively stunning how quickly you can create a fairly thorough test suite for a model. I've included some of my custom assertions so that others don't have quite the barrier to entry that I encounter:
def assert_not_valid(object,
msg="Object is valid when it should be invalid")
assert(!object.valid?, msg)
end
alias :assert_invalid :assert_not_valid
def assert_presence_required(object, field)
# Test that the initial object is valid
assert_valid object
# Test that it becomes invalid by removing the field
temp = object.send field
object.send "#{field}=", nil
assert_invalid object
assert !object.save,
"Cannot save record with invalid #{field}"
assert object.errors.invalid?(field), "Invalid #{field}"
# Make object valid again
object.send("#{field}=", temp)
end
def assert_has_attribute(object, field)
assert object.has_attribute?(field),
"#{object}.klass is missing #{field}"
end
def assert_doesnt_have_attribute(object, field)
assert !object.has_attribute?(field),
"#{object}.klass is includes #{field}"
end
def assert_has_readonly_attribute(class_type, field)
attribute = class_type.readonly_attributes.find { |attr| attr == field }
assert_not_nil attribute,
"#{field} read only attribute was not found."
end
def assert_required_length_less_than(object, field, length)
duplicate = object.dup
# Valid at length-1
duplicate.send "#{field}=", "a"*(length-1)
assert_valid duplicate
# Valid at length
duplicate.send "#{field}=", "a"*length
assert_valid duplicate
# Invalid at length+1
duplicate.send "#{field}=", "a"*(length+1)
assert_invalid duplicate
assert duplicate.errors.invalid?(field),
"Invalid length of : #{field}"
end
def assert_inclusion (object, *list)
assert list.include?(object), "#{object} not in #{list}"
end
def assert_numerical(field)
assert Integer(field) || Float(field),
"The field is not a number."
end
def assert_positive_number(field)
assert_numerical field
assert field > 0 , "The field is not a positive number."
end
def assert_negative_number(field)
assert_numerical field
assert field < 0 , "The field is not a negative number."
end
def assert_uniqueness_of(object, field)
duplicate = object.clone
assert_invalid duplicate,
"The #{field} attribute must be unique."
assert duplicate.errors.invalid?(field), "Invalid #{field}"
end
def assert_inheritance(object_class, object)
assert_kind_of object_class, object,
"#{object} is not a kind of #{object_class}."
end
Pluralization Conventions
One of the things that I needed to get used to was the use of pluralization of objects in Rails. From table names in migrations, to polymorphic has_many associations, being able to specify either single or multiple objects requires being aware of when rails expects plurals. As I mentioned in a previous article, pluralization is controlled through inflections.rb. This is something to reiterate again, unless you want to learn the hard way.
Counter Cache attributes. A counter cache attribute added to a model, is expected to be a pluralized (and possibly inflected) name. For example, to add a Comment object counter cache to another object requires the comments_count attribute to be present.
Limitiations of Single Table Inheritance
The implementation of object inheritance in Rails can only be described as extremely rudimentary. As per Fowler's description of this pattern, a single table is responsible for all base and subclass data. While this is great for subclasses that simply add attributes to a base class when they are subclassed, this does not work so well when derived subclasses have different attributes.
Lack of Class Attribute Encapsulation. Essentially, Rails implementation of STI does not allow subclasses to specify which attributes belong exclusively to themselves. An attribute that is available from one subclass is automatically available to all sibling subclasses. There is no mechanism to disable attribute accessors for certain classes, and enable them for others. It seems the only advantage of STI in rails is to provide convenient way to encapsulate methods and provide a mechanism for storing an object Class name in a table. Not exactly what I would consider Object Oriented .... While it has been suggested that attribute accessors can be overridden within the models, this seems like too much of hack that could have unintended consequences with regards to ActiveRecord behavior. The trade off is to probably code in logic in the classes to ensure that only certain attributes have valid values - the lesser of two evils.
Storage of Class Name as the Base Type in Associations. If polymorphic associations are used in conjunction with STI, the association_type by default must be the base Class name. If it is the actual class name, the association will fail. There are hacks that will allow the actual Class name to be stored by overriding the association_type attribute, but doing so is less than all encompassing. For example, having :dependent => :destroy conditions on the association will fail when this hack is implemented. This would then require manually destroying associated objects. Not really a great compromise in my opinion.
Miscellaneous
Sometimes things just don't work the way you expect them to. In most cases, it's probably just a case of the documentation not being updated with changes in the code. One particularly pernicious bug was the way in which ActiveRecord deals with obj.clone versus obj.dup.
Limitations actually a Good Thing?
While these limitations suggest that these areas of Rails are under developed, Martin Fowler has an interesting take on the rigidity shown by the core rails team to solving "enterprise" problems. In his article he suggests that Rails resistance to antiquated conventions which give strength to the existing system. This goes to prove just because you can do something, doesn't mean you should (such as attempting to create an Object Relational Mapper for Rails - RHibernate anyone?).
I reserve the right to withhold judgment ...


