Bulk testing your model attributes with rails

By | December 12

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, lastnameand age. In order to perfectly test the User model we should write tests which …

  1. 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)
  2. 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:

  1. class User < ActiveRecord::Base
  2.   validates_length_of :firstname, :in => 2..255
  3.   validates_length_of :lastname, :in => 3..255
  4.   validates_numericality_of :age, :only_integer => true
  6.   protected
  8.   def validate
  9.     errors.add(:age, "must be greater 0 and maximum 100") if age.nil? || (1..100).include?(age)
  10.   end
  11. 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:

  1. #tests ONE attribute of the tested model (@model must be set in setup method of the testcase) against given values.
  2. #expecting: what do we expect the values to be (:valid or :invalid)
  3. #attr: the tested attribute (provide an array if the error key differs from the attribute name)
  4. #vals: the values you want to test against
  5. def attr_test(expecting, attr, vals)
  6.   raise ArgumentError, 'model must be set to the testing model' if @model.blank?
  7.   key = att = attr
  8.   att, key = attr[0], attr[1] if attr.is_a? Array
  9.   vals.each do |v|
  10.     m = @model.new(att => v)
  11.     m.valid?
  12.     errors = m.errors.on(key)
  13.     if expecting == :valid
  14.       assert errors.nil?, "'#{v}' should be a valid #{att.to_s} (#{errors})"
  15.     else
  16.       assert !errors.nil?, "'#{v}' should be an invalid #{att.to_s}"
  17.     end
  18.   end
  19. 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.

  1. class UserTest < Test::Unit::TestCase
  2.   def setup
  3.     @model = User
  4.   end
  5.   def test_firstname
  6.     attr_test(:valid, :firstname, ['michal', 'tom', 'John', 'Jennifer K', 'David'])
  7.     attr_test(:invalid, :lastname, [nil, 'J', ''])
  8.   end
  9. end
  10. [/ruby]
  12. 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> ..
  14. [ruby]
  15. #this checks valid and invalid attributes in one go.
  16. #- check attr_test to understand more details if necessary
  17. def bulk_attr_test(attr, values)
  18.   attr_test(:invalid, attr, values[:invalid])
  19.   attr_test(:valid, attr, values[:valid])
  20. end

with this we are ready to rock our User class. See the full test now…

  1. class UserTest < Test::Unit::TestCase
  2.   def setup
  3.     @model = User
  4.   end
  5.   def test_firstname
  6.     bulk_attr_test(:firstname,
  7.       :valid => ['michal', 'tom', 'John', 'Jennifer K', 'David'],
  8.       :invalid => [nil, 'J', ''])
  9.   end
  10.   def test_lastname
  11.     bulk_attr_test(:lastname,
  12.       :valid => ['gabrukiewicz', 'Aniston', 'Heinemeier Hansson'],
  13.       :invalid => [nil, 'gr', 't', ''])
  14.   end
  15.   def test_age
  16.     bulk_attr_test(:age,
  17.       :valid => [1, 10, 20, 73],
  18.       :invalid => [nil, '', 0, -10, -36, x, 7sz, 101, 120])
  19.   end
  20. 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 …

One comment on “Bulk testing your model attributes with rails

  1. 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:

    1. def test_is_root
    2.     bulk_attr_test(
    3.       @client,
    4.       :is_root,
    5.       :valid => [0, 1, true, false],
    6.       :invalid => [nil, '', ' ', 'this is not valid' ,-1, 1.30 ,2, 10]
    7.     )
    8.   end

    My model validation looks like this:

    1. validates_inclusion_of :is_root, :in => [false, true],  :message => "{{value}} is not a valid boolean value"

    Do you have any idea what the problem could be?