Thursday, March 27, 2008

Neat Trick

Often when I'm writing tests, I'll wind up with a series of test cases that are all nearly the same and that try to exercise a single thing in several ways. In this case I find myself with two problems:
  1. I don't really care about the other data in the form as long as it doesn't interfere.
  2. I really hate copying code or data over and over.
So how do we solve this? Well, this may be test code, but it's still just code. So what do we do? Refactor and eliminate duplication - of code AND data.

This is probably easiest to explain by example.

Setup
Let's say we have a registration form with several fields (first name, last name, username, password, and confirm password). At the moment we're interested in testing the first name field. This means we don't care what "last name" is as long as it's valid. Same goes for username, password, and confirm password.

First Attempt
So we write a simple test:
def test_first_name_one_word
num_reg = Registration.count
get :new
put :update, :id => assigns(:registration).id,
:registration => {:first_name => "John",
:last_name => "Smith",
:username => "jsmith",
:password => "12345",
:confirm_password => "12345"
}
assert_equal num_reg + 1, Registration.count
end

(I'm leaving out some assertions just to make this easier.)
Awesome, this is great. My test passes, and life is good.

Problem 1: Many Registrations
Now wait a second. I'm going to have a LOT of registrations by the time I'm done writing the tests for all of these fields. What if my product manager comes back and adds a field then? That's a whole lot of code to change.... ick!

Okay, let's refactor a bit. We'll put the actual registration into a method!

Second Attempt
Create a registerUser method that actually does the registration, then I'll just call it from each of my tests.

def registerUser()
get :new
put :update, :id => assigns(:registration).id,
:registration => {:first_name => "John",
:last_name => "Smith",
:username => "jsmith",
:password => "12345",
:confirm_password => "12345"
}
end

And my tests will call it:
def test_first_name_one_word
num_reg = Registration.count
registerUser()
assert_equal num_reg + 1, Registration.count
end

Cool. That works as far as my what-if-registration-changes scenario.

Problem 2: Changing Data
Well now that I have a separate method I need to handle data that might not always be easy to get. I'm not always going to want the same first name, etc.

Third Attempt
So the first thing we're going to have to do is create a registration object with all the data I need in it. I'll put that in a method so it's really easy to call, override or extend later.

So first I create an object:
def reg_attributes()
{
:first_name => "John",
:last_name => "Smith",
:username => "jsmith",
:password => "12345",
:confirm_password => "12345"
}
end

My registerUser method will then use the object I've created.
def registerUser()
get :new
put :update, :id => assigns(:registration).id,
:registration => :reg_attributes
end

And my tests will call the register method.
def test_first_name_one_word
num_reg = Registration.count
registerUser()
assert_equal num_reg + 1, Registration.count
end

Problem 3: Still Not Changing Data
Okay, my data is all nice and isolated, and I can use it for all sorts of things - registration, login, etc. But I still can't change the data.

Fourth Attempt: The Neat Trick
This is where things get really neat. What I want to do is use the basic object I'd provided myself (the :reg_attributes method), but override the pieces I need to. So what I do is take advantage of a feature in Ruby where if I specify something and there's a default, it will use the one I want (this feature is not only in Ruby, of course, but check what your language does).

So what I'm going to do is provide a way to pass in a hash of overrides. Then I'll just plumb that all the way through and use it in my test.

Here's my override-able object (defaulted to an empty hash):
def reg_attributes(overrides = {})
{
:first_name => "John",
:last_name => "Smith",
:username => "jsmith",
:password => "12345",
:confirm_password => "12345"
}
end

My registerUser method will then take in any overrides I pass it and hand them along to the object I created.
def registerUser(overrides = {})
get :new
put :update, :id => assigns(:registration).id,
:registration => :reg_attributes(overrides)
end

And my tests will call the register method with any overrides that it wants.
def test_first_name_one_word
num_reg = Registration.count
registerUser(:first_name => "Mary Jane")
assert_equal num_reg + 1, Registration.count
end

The advantage to doing it as a hash is that it can have many members, or just one, or none at all. So I can pass in a first name, a last name, or both equally easily.

Pretty neat, huh?


*Note: All examples in this entry are in Ruby. Pick your poison; the principles apply to other languages.

No comments:

Post a Comment