DRY unit tests

I’m a huge fan of Test-driven development (TDD) because it frees me from the fear of refactoring. Projects evolve. Change is the only certainty. When I’ve got a complete test suite including units, functionals, integration, and API tests I can tweak code and not fear breakage.

With this in mind it’s strange that I hadn’t thought more about refactoring my test suites. I just cranked out tests one by one. My test coverage (rake stats) soared above 1:1. Everything looked great. Then a few days ago I revisited a certain unit test and almost gagged at the amount of code repetition.

I had hurried through writing my test cases focusing only on completeness, not on beauty and DRYness. So with the hope that this will inspire someone else who has been neglecting their test suites, here is a before and after code snapshot…

Example unit tests (old way)

require File.dirname(__FILE__) + '/../test_helper'

class UserTest < Test::Unit::TestCase

  # --- [ username ] ---
  def test_username_blank
    user = build_user('', '5550980', 'franky', 'franky.grouch@aol.com')
    assert_raise(ActiveRecord::RecordInvalid) { user.save! }
    assert_not_nil user.errors.on(:username)  
  end
 
  def test_username_too_short
    user = build_user('fr', '5550980', 'franky', 'franky.grouch@aol.com')
    assert_raise(ActiveRecord::RecordInvalid) { user.save! }
    assert_not_nil user.errors.on(:username)    
  end
    
  def test_username_too_long
    user = build_user('0123456789012345678901234567890', '5550980', 'franky', 'franky.grouch@aol.com')
    assert_raise(ActiveRecord::RecordInvalid) { user.save! }
    assert_not_nil user.errors.on(:username)    
  end

  ...
	
  private

  def build_user(username, password, screen_name, email_address)
    User.new(:username => username, :password => password, :screen_name => screen_name, :email_address => email_address)
  end
	
end

Example unit tests (new way)

require File.dirname(__FILE__) + '/../test_helper'

class UserTest < Test::Unit::TestCase
  fixtures :users

  def setup
    @user = User.new(:username => 'victorgray', 
                     :password => 'bigtimesecret', 
                     :screen_name => 'Victor Gray', 
                     :email_address => 'victor.gray@gmail.com')
  end
  
  def test_username
    assert_errors_on_field(@user, :username, '') # blank
    assert_errors_on_field(@user, :username, 'vic') # too short
    assert_errors_on_field(@user, :username, 'victor_has_a_majorly_too_long_username') # too long
    assert_errors_on_field(@user, :username, 'victor gray') # with space
    assert_errors_on_field(@user, :username, users(:first).username) #duplicate
    assert_errors_on_field(@user, :username, 'admin') # reserved name
    assert_success_with_field(@user, :username, 'victor_gray') # ok
  end

  ...

end

test_helper.rb

ENV["RAILS_ENV"] = "test"
require File.expand_path(File.dirname(__FILE__) + "/../config/environment")
require 'test_help'

class Test::Unit::TestCase

  ...
  
  # --- [ unit test helpers ] ---
  def assert_errors_on_field(record, field, value)
    record.send field.to_s + '=', value
    assert_raise(ActiveRecord::RecordInvalid) { record.save! }
    assert_not_nil record.errors[field.to_sym]
  end

  def assert_success_with_field(record, field, value)
    record.send field.to_s + '=', value
    assert_nothing_raised{ record.save! }
    assert record.errors.empty?
  end

  ...
	
end

Much nicer I think. Test-driven development allows for fearless code refactoring… just don’t forget to refactor your tests as well! Hope this helps.

Comment or question via
FYI: This post was migrated over from another blogging engine. If you encounter any issues please let me know on . Thanks.