Quest for the Holy Rails

May 2, 2007

Would you Jump Before you Checked your Parachute?

Filed under: design decisions, rails — Jake Brownson @ 7:59 am

Software engineering has one big difference from all other engineering disciplines. If a civil engineer built a bridge without going through computer simulations, scale models, and rigorous review he would be jailed. If an aerospace engineer thought he found a new wing design and threw it on a passenger jet to see if it would work he and his grandchildren would be barred from engineering forever. Nobody thinks twice when a software engineer whips up a new function and runs it to see if it works. In this post I’ll introduce the concept of test driven development and explain why I will be using it for my Pente project.

Part of the agile/extreme programming movement is the notion of test driven development (TDD). The crux of the idea is that before you write any application code you should first write a test that fails because the code doesn’t exist. The big idea is Red, Green, Refactor. The official dogmatic TDD method is a bit more strict than I will demonstrate here, but this is how I have personally found the ideas of TDD to be useful.

This article assumes you are familiar with the basic Unit Testing features of Rails. There is a great resource on the Rails wiki that will help you learn what you need.

I highly recommend the book Test Driven Development by Example. It does a great job of introducing the need for test driven development and introducing the technique step by step.

1: Red

So you want your program to do something. Let’s say you want your Player model to validate the format of the email address. The first step is to write a test that fails because the Player model doesn’t validate the format of the email address.

def test_validate_email_invalid
  p = Player.new
  p.email = '@bedrock.com'
  assert !p.valid?
end

Run the test and watch it fail miserably. You’re now in the red! Don’t worry though, this is a good thing. You’re making progress. You’ve now defined how you want your code to work. You also know that your test fails properly. This is really important. If you write a test and never actually see it fail how do you know the test is working? The next step is to make it pass!

2: Green

The official dogmatic TDD approach is to write the dirtiest, ugliest, quickest code you can that will make your test pass. Personally I think that’s a bit much. One of the nice things about TDD is that if you are having trouble with a certain section of code, or want to be really sure that a piece of code is working perfectly you can always slow down and start doing really detailed tests, but if you’ve done a thing a thousand times and know it will probably work right away you can test in big chunks.

Let’s make our email validation test pass by adding this code to our model:

validates_format_of :email, :with => /.+@.+..+/

Run your test again and you’ll see that we’re passing! We’re back in the green!

3: Refactor

In the refactor phase you get the chance to make your code nicer. You can break out functions/classes and make your code look nicer. Since you’ve got all the things your code is supposed to do under test you’ll know you aren’t breaking any established functionality. Don’t forget to refactor the tests themselves too. Our example isn’t really complicated enough yet to require any refactoring, so let’s move on and dig ourselves back in the red.

4: Red

Our simple test got us some basic email validation, but there are still lots of bad addresses that will get through. Let’s improve it a bit:

def test_validate_email_invalid
  p = Player.new
  p.email = 'fl1nst0n3@bedr0ck.c0m'
  assert !p.valid?
  p.email = 'fflinstone@bedrock.theoffice'
  assert !p.valid?
  p.email = 'fflinstone'
  assert !p.valid?
  p.email = 'fflinstone@bedrock'
  assert !p.valid?
  p.email = '@bedrock.com'
  assert !p.valid?
  p.email = 'fflinstone@bedrock.c'
  assert !p.valid?
  p.email = 'fflin@stone@bedrock.com'
  assert !p.valid?
  p.email = 'fflinstone@bed_rock.com'
  assert !p.valid?
  p.email = 'fflinstone@bedrock..com'
  assert !p.valid?
  p.email = ''
  assert !p.valid?
  p.email = nil
  assert !p.valid?
end

def test_validate_email_valid
  p = Player.new
  p.email = 'fflinstone@bedrock.com'
  assert p.valid?
  p.email = 'fflinstone@bedrock.co.uk'
  assert p.valid?
  p.email = 'fflin_ston-e@bed-rock.com'
  assert p.valid?
  p.email = 'fflinstone@bedrock.museum'
  assert p.valid?
  p.email = 'fl1nst0n3@bedr0ck.com'
  assert p.valid?
  p.email = 'FLiN570nE@B3dR0ck.com'
  assert p.valid?
end

The modified test takes a big chunk out of our email validation problem by throwing in a whole bunch of possible problems with an email address. I took a bigger chunk because I have a regular expression that I’ve used before that I’m confident will solve this on the first try. If I didn’t I might just throw one problem in at a time and build a regular expression that would solve each new problem while not breaking old ones.

5: Green

Here’s the new code for the model:

validates_format_of :email, :with => /A[A-Z0-9._%-]+@(?:[A-Z0-9-]+.)+[A-Z]{2,6}Z/i

We should be passing all of our tests again, but the tests are a bit ugly, let’s move on.

6: Refactor

Now we can make our test code look a little nicer:

def test_validate_email_invalid
  p = Player.new
  ['fl1nst0n3@bedr0ck.c0m',
   'fflinstone@bedrock.theoffice',
   'fflinstone',
   'fflinstone@bedrock',
   '@bedrock.com',
   'fflinstone@bedrock.c',
   'fflin@stone@bedrock.com',
   'fflinstone@bed_rock.com',
   'fflinstone@bedrock..com',
   '',
   nil].each do |bad_email|
    p.email = bad_email
    assert !p.valid?
  end
end

def test_validate_email_valid
  p = Player.new
  ['fflinstone@bedrock.com',
   'fflinstone@bedrock.co.uk',
   'fflin_ston-e@bed-rock.com',
   'fflinstone@bedrock.museum',
   'fl1nst0n3@bedr0ck.com',
   'FLiN570nE@B3dR0ck.com'].each do |good_email|
    p.email = good_email
    assert p.valid?
  end
end

Now What?

Keep adding tests to express the desired behavior of your code, then make those tests pass. You’ll wind up with a fully tested codebase that makes adding new features and refactoring the code a dream because you can run the tests to know what functionality you’ve changed.

TDD is very hard at first, but if you stay committed and keep writing your tests before the code that makes them work it will become a natural process and writing untested code will start to give you the shivers.

Questions

  • What do you think about TDD?
  • If you’ve used TDD in a project how did it go?
  • Have you ever had to refactor testless code?

Where to Next?

Read about my decision to use RESTful design or follow along as I implement the project.

Advertisements

Leave a Comment »

No comments yet.

RSS feed for comments on this post. TrackBack URI

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Blog at WordPress.com.

%d bloggers like this: