When it comes to testing in Ruby on Rails I tend to test every piece of its public interface. This means I test every single public method and attribute to ensure my desired functionality. Recently I’ve updated my application from Rails 1.2.5 to 2.0.1 and I was happy to have tests. None of them failed and so I knew that my application is ready to rock with the new Rails 2.0.
However, what I want to write in this article about is how to do bulk testing of each single attribute of your models. Let’s be a bit precise. Say we have a model User
which has attributes firstname
, lastname
and age
. In order to perfectly test the User
model we should write tests which …
- ensure that invalid values result in an error for the tested attribute (e.g. age -2 is invalid, so the instance should hold an error for the attribute
age
) - and valid values don’t result in an error for the tested attribute (e.g. age 20 should be fine)
Usually you should test a couple of invalid values and a couple of valid values against each attribute of your model. This can be quite a lot to code. I explain a nice approach how to speed things up and save time for the next family event.
(Code below has been tested with Rails 1.2.5 and Rails 2.0.1) Let’s say our User class looks the following:
- class User < ActiveRecord::Base
- validates_length_of :firstname, :in => 2..255
- validates_length_of :lastname, :in => 3..255
- validates_numericality_of :age, :only_integer => true
- protected
- def validate
- errors.add(:age, "must be greater 0 and maximum 100") if age.nil? || (1..100).include?(age)
- end
- end
I don’t go too much into detail about this. Simply: It’s a User
which is bound to a Users
database table and includes three validations:
- the firstname should have at least 2 chars and maximum 255
- the firstname should have at least 3 chars and maximum 255
- the age must be a number greater than 0 and maximum 100
Best case would be if we completele test the User
model for the following cases (testing that wrong values fail and correct values don’t fail):
- valid firstnames:
michal, tom, John, Jennifer K, David
- invalid firstname:
nil, J, ''
- valid lastnames:
gabrukiewicz, Aniston, Heinemeier Hansson
- invalid lastnames:
nil, gr, t, ''
- valid ages:
1, 10, 20, 73
- invalid ages:
nil, '', 0, -10, -36, x, 7sz, 101, 120
- creating a valid User and successfully saving (not described in this article)
- get an existing user, modify some attributes and save (not described in this article))
Here we defined what is wrong and what not (what is even better: you could write this tests even before you write your User
model). In order to do the bulk testing I have written a helper method which will do the stuff for us in a Rails-way. We put this method into our test_helper.rb
so its available to all our test cases:
- #tests ONE attribute of the tested model (@model must be set in setup method of the testcase) against given values.
- #expecting: what do we expect the values to be (:valid or :invalid)
- #attr: the tested attribute (provide an array if the error key differs from the attribute name)
- #vals: the values you want to test against
- def attr_test(expecting, attr, vals)
- raise ArgumentError, 'model must be set to the testing model' if @model.blank?
- key = att = attr
- att, key = attr[0], attr[1] if attr.is_a? Array
- vals.each do |v|
- m = @model.new(att => v)
- m.valid?
- errors = m.errors.on(key)
- if expecting == :valid
- assert errors.nil?, "'#{v}' should be a valid #{att.to_s} (#{errors})"
- else
- assert !errors.nil?, "'#{v}' should be an invalid #{att.to_s}"
- end
- end
- end
You see that the method expects the attribute we want to test (attr
), the tested values (vals
) and what we expect the values to be (expecting
) - valid or invalid. Furthermore you see that the tested model is taken from the instance variable (@model
) which will be taken from your individual test case (placed into the setup
method). Thats to stick with DRY (cause every unit test deals with one model). Last but not least the helper asserts with a nice error message which eases us up to find the problem .. if there is one.
Ok enough, we can use this now for the test of our User
. Rails generated the UserTest
for us when we generated the model. We test our attributes firstname
, lastname
and age
. All for validity and invalidity.
- class UserTest < Test::Unit::TestCase
- def setup
- @model = User
- end
- def test_firstname
- attr_test(:valid, :firstname, ['michal', 'tom', 'John', 'Jennifer K', 'David'])
- attr_test(:invalid, :lastname, [nil, 'J', ''])
- end
- end
- [/ruby]
- Thats nice but at this point we stop. We are not testing <code>lastname</code> and <code>age</code> anymore because before we do so we DRY this a bit up. We add another helper method which uses our existing <code>attr_test</code> and tests the invalid and valid values in one go. After that we sit back and watch our tests run. Bung this method into your <code>test_helper.rb</code> ..
- [ruby]
- #this checks valid and invalid attributes in one go.
- #- check attr_test to understand more details if necessary
- def bulk_attr_test(attr, values)
- attr_test(:invalid, attr, values[:invalid])
- attr_test(:valid, attr, values[:valid])
- end
with this we are ready to rock our User
class. See the full test now…
- class UserTest < Test::Unit::TestCase
- def setup
- @model = User
- end
- def test_firstname
- bulk_attr_test(:firstname,
- :valid => ['michal', 'tom', 'John', 'Jennifer K', 'David'],
- :invalid => [nil, 'J', ''])
- end
- def test_lastname
- bulk_attr_test(:lastname,
- :valid => ['gabrukiewicz', 'Aniston', 'Heinemeier Hansson'],
- :invalid => [nil, 'gr', 't', ''])
- end
- def test_age
- bulk_attr_test(:age,
- :valid => [1, 10, 20, 73],
- :invalid => [nil, '', 0, -10, -36, x, 7sz, 101, 120])
- end
- end
That’s it if the tests were successful then your model is working fine. If not then you will see helpful messages in your console which will indicate what went wrong during the attributes test.
I strongly recommend you to test all your models against the functionality you will use in your application(s) (all public class members). This takes a bit of time but it let’s you sleep better after doing any upgrades, enhancements, bug fixes etc. And don’t forget: never cheat on yourself when writing tests! you are writing them for you …
hi, nice post. I’ve implemented your work to learn more about TTD.
But I seem to have stumbled upon a problem. When I want to test for an attribute witch should be a boolean, I can’t seem to get my test to pass:
My model validation looks like this:
Do you have any idea what the problem could be?