A Rails HOWTO: Simplify Your Unit Tests
So you’ve spent a few weeks writing your Ruby on Rails application, and then you realize you’ve been spending all of your time learning about this awesome language and web development framework, and all of the automagically generated unit tests that Rails makes for you are really just stubs. Damn. You sit down and start coding, but you realize you’re writing variations on a theme: check to see if a model creates/updates/destroys correctly, etc. etc. You remember something DHH said about a TLA… that’s right! DRY!
So in the giving spirit of the ancient holiday of “Hey Where The Hell Did The Sun Go Goddamnit,” here are a few useful and flexible helper methods for streamlining those unit tests.
Birth: Testing Creation
Ah, the arrival of the stork and a bouncing baby model! What do you do when you get a new baby? Spank it to make it cry and check it for ten fingers and toes! Same goes for models, really. You want to make sure that your model is actually reading from the database since that’s something most people assume it should do, and it’s definitely the kind of thing you want advance notice of should it stop working.
So crack open your test/test_helper.rb and toss this method in the Test::Unit::TestCase declaration:
def test_creation_of(options)
raise NotTheRightOptions.new('Options must include a record, a fixture, and an array of attributes') if (options.keys | [:record, :fixture, :attributes]).size > options.size
assert_kind_of options[:model], options[:record] if options[:model]
for attr in options[:attributes]
assert_equal options[:record][attr], options[:fixture].send(attr), "Error matching #{attr}"
end
end
Be sure to add a NotTheRightOptions declaration before Test::Unit::TestCase:
class Test::Unit::TestCase::NotTheRightOptions < Exception; end
And when you want to test a model’s creation, it’s as simple as this:
def test_create
test_creation_of :model => Person,
:record => @person,
:fixture => @mr_happy_person,
:attributes => [:name, :address, :phone_number, :date_of_birth]
end
That checks @person, an instance of the model you’re testing, against the fixture element @mr_happy_person, using the attributes name, address, phone_number, and date_of_birth. It also makes sure that the record is an instance of the model class. Neat, huh? And the :model option is optional; it just makes for an additional test if need be.
Love: Testing Updating
Your little baby model has become a young adult, and is ready to start making its own choices. How can you be sure that those choices are the right ones? Hah, just kidding. It’ll smoke weed regardless of what you do. The best thing you can do as the model’s decreasingly-proud parent is make sure whatever values you instill in it don’t get thrown to the wayside after the first plastic cup of beer.
Sounds like a job for a helper method.
def test_updating_of(options)
raise NotTheRightOptions.new('Options must include a record, a fixture, and an attribute') if (options.keys | [:record, :fixture, :attribute]).size > options.size
options[:record][options[:attribute]] = options[:fixture]
assert options[:record].save, options[:record].errors.full_messages.join('; ')
options[:record].reload
assert_equal options[:fixture], options[:record].send(options[:attribute])
end
This one’s a bit simpler: given a record, a fixture (this time a value, not a fixture instance), an an attribute, it alters the attribute, saves the changes, reloads the record, and checks to see that the changes were saved.
Behold:
def test_update
test_updating_of :record => @person,
:fixture => 'Namey McTesterton',
:attribute => :name
end
Death: Testing Destruction
I always get a little teary about this, so I’ll keep it short. Our model was a dear one, but if it’s dead we want it to stay dead. Resurrection is for Hollywood zombies and Easter only.
def test_destruction_of(options)
raise NotTheRightOptions.new('Options must include a record and a model') if (options.keys | [:record, :model]).size > options.size
options[:record].destroy
assert_raise(ActiveRecord::RecordNotFound){ options[:model].find(options[:record].id) }
end
And we’re off:
def test_destroy
test_destruction_of :model => Person,
:record => @person
end
Happy Hey Where The Hell Did The Sun Go Goddamnit!
So there you go; a Hey Where The Hell Did The Sun Go Goddamnit gift from me to you, to save you the boredom and carpal tunnel problems associated with unit testing. It’s not a glamorous job, but it sure beats having a lot of bugs in your code.
And remember: there are productivity boosts, but there are no free lunches (especially at those conferences I keep hearing about). Unit tests match the intention of the programmer with the reality of the code. There is no way to perfectly automate this, and nothing’s worse than crappy unit tests–a false negative is worse than a false positive. So while helper functions can reduce the repetition of unit tests, they can’t replace it.
Happy Hey Where The Hell Did The Sun Go Goddamnit to all, and to all a swift return to reasonable levels of vitamin D!
January 13th, 2006 at 7:24pm
[...] Simplify your Rails Unit Tests [...]
April 28th, 2006 at 12:20am
[...] Enter Sweet Trick Number 1 (one which I cannot take credit for): Coda Hale gives some great instructions on how to abstract your CRUD testing into Rails’ test_helper.rb class. By pulling the repetitive assertions out, you’re left with clean Unit Tests that can easily be copied as you create new Models. [...]
August 14th, 2006 at 3:49pm
Why bother testing create/destroy or any other ActiveRecord provided functionality? That functionality is already tested in ActiveRecord’s own unit tests.
You should instead be focussing on testing your model’s behaviour, your business rules, constraints, validations etc.
August 24th, 2007 at 10:34am
Re: Luke Redpath
Test ActiveRecord provided functionalities is very interesting, because many plugins (including that ones you create) and directives, change the behavior of this methods.
On the past i think just like you, that made in methods should not be teste, so i found a biggest bug on a plugin that i was create.
One another interesting bug that i find because my testes:
def test_find_update
j = User.find(@luke.id)
j.name = "Yoda"
j.save
j = User.find(@luke.id)
assert_equal(users('luke')[:password], j.password)
end
At first time this test must apear crazy, but this was created to test if password was be reencrypted on before_save if it was not changed.