Testing in Rails: Part 9 - Attributes and Callbacks

This is part of an ongoing series of posts about how to get started writing tests for Ruby on Rails. The series begins with an introduction and overview of the ideas behind testing.
In previous sections, we learned to unit test the ActiveRecord associations and validations in our classes. Both are extremely common and appear in most Rails models. While less common, attribute definition methods and callbacks are still used somewhat frequently and are worth learning to test.
Hopefully by now your knowledge and confidence about writing tests is growing!
Attribute Definition Methods
When I refer to attribute definition methods, a classification I made up, I am referring to the methods that define and describe attributes in our model. There are two kinds. First, there are methods in the Ruby language that define attribute reader and writer methods: attr, attr_accessor, attr_reader, and attr_writer. Second, there are methods in the Rails framework that limit the access to attributes: attr_accessible, attr_protected, and attr_readonly.
Attribute Reader/Writer Methods
The first type of methods add and define attributes in a class. The class might not use a database at all, like our Car model. A shopping cart is a common example of a class with defined attributes which might not use ActiveRecord. On the other hand, the class might use a database but define some additional attributes that do not exist in the database. These attributes might be for convenience or temporary use by our class. In both cases, these attributes need unit tests.
We discussed how to test these accessor, reader, and writer attributes already in Part 3 of this tutorial. It is no different now that we are inside Rails—test that the method does not raise an exception (with assert_nothing_raised) and test that the attribute value is what was expected.
Attribute Access Limiting Methods
Remember that ActiveRecord already handles the attribute methods for any database fields. Our model for Wine has year and year= as instance methods, even though we never explicitly defined them, because the Wine model is a subclass of ActiveRecord::Base. As I explained previously, ActiveRecord already has its own unit tests, so we will not need to test those attribute methods again.
However, we can use some Rails attribute methods to limit access to the attributes that ActiveRecord is managing using attr_accessible, attr_protected, and attr_readonly. These methods are used primarily to limit the ways in which users can update the values of table fields. They are special-case, attribute security methods. If you use them, they need unit testing to make sure you have used them properly.
For example, it is a common mistake for developers to think that attr_protected somehow protects an attribute from being read or written. It does not. It only protects the attribute from “mass assignment”. Mass assignment occurs when a hash, usually parameters from form data, are passed to an ActiveRecord class in one big pile and then ActiveRecord handles sorting through the pile to set the right the attributes. Wine.new(params[:wine]) and Wine.update(params[:wine]) are examples of mass assignment in practice. It is convenient to be able to pass the whole pile of parameters to ActiveRecord, but it opens up the possibility that a hacker could submit a modified form with additional, unexpected parameters and get those attributes updated at the same time. Imagine that we let users update some small part of our wine model with a form, but we do not let them modify the year. Unprotected, a bogus form could be submitted that contains a field for :year and ActiveRecord would happily update it. If :year were attr_protected then :year would not be set regardless of what parameters were passed to ActiveRecord’s update method. At the same time, wine.year=2005 in our code would still succeed.
attr_accessible is the opposite of attr_protected. It specifies only the attributes which can be mass assigned. Every other attribute not listed is considered attr_protected.
attr_readonly is not commonly used. In fact, you may never come across it. It allows the attribute to be writable only on a new record. After that it becomes read only. Both mass assignment and explicitly calling it with wine.year=2005 will not update it.
Testing Attribute Definition Methods
Our Wine and Winery models do not make use of either new attributes or protected attributes. Rather than create a contrived usage to make the point, the best example is the common implementation for a User model. (You can add a ‘users’ table and a model for User to your application, but if you just follow along I think you will get the point.) Here is a simplified version:
attr_accessor :password
attr_protected :hashed_password
def before_create
self.hashed_password = User.hash_password(@password)
end
def after_save
@password = nil
end
def self.authenticate( username = "", password = "" )
user = self.find(:first, :conditions => ["username = ?", username])
return (user && user.authenticated?(password)) ? user : nil
end
def authenticated?( password = "" )
self.hashed_password == User.hash_password(password)
end
private #-------------
def self.hash_password( password )
return Digest::SHA1.hexdigest(password)
end
end
The attribute hashed_password exists in the database table ‘users’ but, because of attr_protected, it cannot be mass assigned a value. Instead, we define a new attribute that does not exist in the database table, password, and which can be accessed, even through mass assignment. Then when a user is saved into the database, callbacks will set hashed_password to the encrypted value of password, save the record, and then set password to nil. It provides a smooth way for passwords to get encrypted while also decreasing access to the encrypted version.
Unit tests for the attribute definitions in this model might look like this:
fixtures :users
def setup
@user = users(:user_00002)
end
def test_attr_accessor
new_user = User.new
# attribute should initialize to nil
assert_nil(new_user.password)
# attribute should allow writing
assert_nothing_raised { new_user.password = "test" }
# attribute should allow reading
assert_nothing_raised { new_user.password }
# ...and the value should be what is expected
assert_equal("test", new_user.password)
end
def test_attr_protected
# protected attributes cannot be mass assigned when creating
new_user = User.new(:username => 'test', :hashed_password => 'xyz123')
assert_equal('test', new_user.username) # assigned
assert_equal(nil, new_user.hashed_password) # not assigned
# protected attributes cannot be mass assigned when updating
@user.update_attributes(:username => 'test', :hashed_password => 'xyz123')
assert_equal('test', @user.username) # changed
assert_equal("43f0ae2625a6a1cea274acb69125708170719541",
@user.hashed_password) # not changed
# protected attributes CAN be directly assigned
@user.hashed_password = "456abc"
assert_equal("456abc", @user.hashed_password)
end
end
In test_attr_protected, we can prove using test assertions that the protected attribute cannot be mass assigned but can be directly assigned. If we wanted to test attr_accessible instead we could perform a mass assignment then test that the accessible attributes were updated but that any other attributes were not.
Testing Callbacks
Callbacks, like custom methods, can be enlisted to perform any number of tasks. What is unique about callback is when they happen. It will remain up to you to test whatever custom task they perform. So, fundamentally, we need to test two things about callbacks: what they do and when they do it.
Let’s add a simple before_save callback to our Wine model.
...
def before_save
# year cannot be larger than the current year
self.year = [self.year, Time.now.year].min
end
end
I realize the callback could be handled better by a validation, but it makes the point. Before a wine is saved to the database, the before_save callback will make sure that the year is not larger than the current year. If it is, year will be set to the current year instead.
Here is how we can test the callback.
...
def test_before_save
# test year limit when creating a new record
new_wine = Wine.new(:name => 'Super Sweet',
:family => 'Ice Wine', :winery_id => 1,
:year => 2.years.from_now.year)
assert_equal(2.years.from_now.year, new_wine.year)
new_wine.save
assert_equal(Time.now.year, new_wine.year)
# test year limit when updating a existing record
@bordeaux.year = 2.years.from_now.year
assert_equal(2.years.from_now.year, @bordeaux.year)
@bordeaux.save
assert_equal(Time.now.year, @bordeaux.year)
end
end
Of course, you could test other years too. But in my view there are two important tests that must be run. First, test that before saving :year has one value and after saving it has another. That way we know that the before_save method did its job. Second, test that before_save does its job both when creating and when updating. Why? Because the before_save callback should occur before both operations, while before_create and before_update occur only before each one respectively. We might reveal a big bug in our application if we discover that we used the wrong one!
Other callbacks get tested the same way. Test whatever the callback should accomplish, but also test to be sure that it happens when you expect it to happen. In general, I would recommend testing all callbacks related to creating and updating records for both cases. It is easy to use the wrong callback and, since your code still works in the other case, it may be hard to spot.
UPDATE: The series continues in Part 10.

February 11th, 2008 at 6:45 am
[...] Testing in Rails: Part 9 - Attributes and Callbacks - More in the ongoing series from Null is Love. [...]