Saturday, May 2, 2009

Testing model in rails

How we want our test environment

  1. Isolation of input fixtures, so that changing fixture of one test code will not impact others.
  2. Easily create input set and reuse existing.
  3. Readable test code.
  4. Runs very fast(Slower test codes are run less by developers).
  5. Write less to test.

Problems with built in fixture and test helper

  1. Fixture does not allow to isolate input set.
  2. No feature of reusability in fixtures causes developer to create unnecessary duplication in fixtures.
  3. Changes in table definition causes a lot of change in fixture set.
  4. Fixtures are hidden in different file. So it becomes difficult to trace a test code.
  5. Fixture is database driven. So even if you do not need a saved instance of an ActiveRecord object, you end up using one. The more the queries get executed, the slower the test code becomes.

Factory girl, one of the alternate of fixture seems to be the better solution then fixture. And shoulda makes the life easier for writing test codes.

Why factory?

  1. Isolated input set
  2. Different build method gives the flexibility to create saved and unsaved instances
  3. Can easily extend existing factory to create a new one
  4. Facility to override property and to stub methods to reduce database query
  5. Increase readability of test codes.

Why Shoulda?

  1. Context & Should blocks - Context and should block provides facilities to do BDD
  2. Assertions - Provides many common and useful assertions
  3. Macros - Generate many ActionController and ActiveRecord tests with helpful error messages

So let’s get some action. Here is a simple test done using factory and shoulda. We will write unit test for User model which looks like the following.

class User < ActiveRecord::Base
 has_many :posts, :order => 'created_at DESC'
 has_many :comments

 validates_format_of :email, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i
 validates_uniqueness_of :email

 def display_name
   return "#{first_name} #{last_name}"
 end

 def latest_post
   return self.posts.first
 end
end

The Post model is,

class Post < ActiveRecord::Base
 belongs_to :user
 has_many :comments, :dependent => :destroy

 validates_presence_of :title
 validates_presence_of :body
end

And the user factory is,

Factory.sequence :email do |n|
 "email#{n}@test.com"
end

Factory.define :user do |u|
 u.first_name 'Ashraf'
 u.last_name  'Zaman'
 #allows you to create different users with different email using the same definition
 u.email { Factory.next(:email) }
end

Post factory is,

Factory.define :post do |p|
 p.title 'A sample post'
 p.body  'This is a sample post'
 p.created_at Time.now
 p.updated_at Time.now
end

Finally the user model test is,

require 'test_helper'

class UserTest < ActiveSupport::TestCase

 should_have_many :posts
 should_have_many :comments
 should_require_unique_attributes :email

 should_not_allow_values_for :email, "ab.cd", "1234@"
 should_allow_values_for :email, "ashrafuzzaman.g2@gmail.com"

 should 'display_name' do
   user = Factory.build(:user, :first_name => 'Ashraf', :last_name => 'Zaman')
   assert_equal('Ashraf Zaman', user.display_name)
 end

 context 'Default posts' do
   setup do
     @user = Factory.build(:user, :posts => [
         Factory.build(:post, :title => 'Second Post', :created_at => Time.now),
         Factory.build(:post, :title => 'First Post', :created_at => 2.minutes.ago)
       ])
   end

   should 'return latest post' do
     assert_equal('Second Post', @user.latest_post.title)
   end
 end

end

You can download the demo from here

How factory helped

To test the display_name I do not need a saved instance, So I used Factory.build to build the model object with out saving it in the database.

Note: With Factory.build you can create an unsaved model instance from the factory prototype. It is very useful for testing model logic.

Some attributes of user prototype are intentionally redefined to increase the readability of the test code. Now you can easily read the test code as you can see the input set right in front of you. Thus you can effortlessly create different input set from one basic prototype.

The interesting part of this section is the test of latest_post of user model. Here as you can see the association method ‘posts’ is overridden with expected post list. Now you can mock this this methods as this is provided by the framework itself. So no need to test it. But by mocking this you actually increased the readability of the test code and avoided database call.

How shoulda helped

The Shoulda gem makes it easy to write elegant, understandable, and maintainable Ruby tests. Shoulda consists of test macros, assertions, and helpers added on to the Test::Unit framework.

With shoulda we defined a context 'Default posts' and initialized it. We can test codes related to posts with in the context. As the context is setup with the minimum and sufficient information of input set needed for the context, the test code becomes very simple and easy to read and maintain.

Resources

http://thoughtbot.com/projects/factory_girl

http://thoughtbot.com/projects/shoulda

No comments: