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.


Commenting is closed for this article.