Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / Ruby

Rails 3 in Action - Test-Driven Development

4.50/5 (2 votes)
26 Sep 2011CPOL8 min read 18.3K  
A Chapter excerpt from Rails 3 in Action
image002.jpgRails 3 in Action
By Yehuda Katz and Ryan A. Bigg

A cryptic yet true answer to the question “Why should I test?” is “because you are human.” Because humans make mistakes, having a tool to inform them when they make one is helpful, isn’t it? In this article based on chapter 2 of Rails 3 in Action, the authors show you how to save your bacon with test-driven development.

You may also be interested in…

A cryptic yet true answer to the question "Why should I test?" is "because you are human." Humans—the large majority of this book’s audience—make mistakes. It’s one of our favorite ways to learn. Because humans make mistakes, having a tool to inform them when they make one is helpful, isn’t it? Automated testing provides a quick safety net to inform developers when they make mistakes. By they, of course, we mean you.

We want you to make as few mistakes as possible. We want you to save your bacon! TDD and BDD also give you time to think through your decisions before you write any code. By first writing the test for the implementation, you are (or, at least, you should be) thinking through the implementation: the code you’ll write after the test and how you’ll make the test passes. If you find the test difficult to write, then perhaps the implementation could be improved. Unfortunately, there’s no clear way to quantify the difficulty of writing a test and working through it other than to consult with other people who are familiar with the process.

Once the test is implemented, you should go about writing some code that your test can pass. If you find yourself working backward—rewriting your test to fit a buggy implementation—it’s generally best to rethink the test and scrap the implementation. Test first, code later.

Why test?

Automated testing is much, much easier than manual testing. Have you ever gone through a website and manually filled in a form with specific values to make sure it conforms to your expectations? Wouldn’t it be faster and easier to have the computer do this work? Yes, it would, and that’s the beauty of automated testing: you won’t spend your time manually testing your code because you’ll have written test code to do that for you.

On the off chance you break something, the tests are there to tell you the what, when, how, and why of the breakage. Although tests can never be 100% guaranteed, your chances of getting this information without first having written tests are 0%. Nothing is worse than finding out something is broken through an early-morning phone call from an angry customer. Tests work toward preventing such scenarios by giving you and your client peace of mind. If the tests aren’t broken, chances are high (though not guaranteed) that the implementation isn’t either.

You’ll likely at some point face a situation in which something in your application breaks when a user attempts to perform an action you didn’t consider in your tests. With a base of tests, you can easily duplicate the scenario in which the user encountered the breakage, generate your own failed test, and use this information to fix the bug. This commonly used practice is called regression testing.

It’s valuable to have a solid base of tests in the application so you can spend time developing new features properly rather than fixing the old ones you didn’t do quite right. An application without tests is most likely broken in one way or another.

Writing your First Test

The first testing library for Ruby was Test::Unit, which was written by Nathaniel Talbott back in 2000 and is now part of the Ruby core library. The documentation for this library gives a fantastic overview of its purpose, as summarized by the man himself:

The general idea behind unit testing is that you write a test method that makes certain assertions about your code, working against a test fixture. A bunch of these test methods are bundled up into a test suite and can be run any time the developer wants. The results of a run are gathered in a test result and displayed to the user through some UI.

—Nathaniel Talbott

The UI Talbott references could be a terminal, a web page, or even a light.[1]

A common practice you’ll hopefully by now have experienced in the Ruby world is to let the libraries do a lot of the hard work for you. Sure, you could write a file yourself that loads one of your other files and runs a method and makes sure it works, but why do that when Test::Unit already provides that functionality for such little cost? Never reinvent the wheel when somebody’s done it for you.

Now you’re going to write a test, and you’ll write the code for it later. Welcome to TDD.

To try out Test::Unit, first create a new directory called example and in that directory make a file called example_test.rb. It’s good practice to suffix your filenames with _test so it’s obvious from the filename that it’s a test file. In this file, you’re going to define the most basic test possible, as shown in the following listing.

Listing 1 example/example_test.rb

C++
require 'test/unit'

class ExampleTest < Test::Unit::TestCase
  def test_truth
    assert true
  end
end

To make this a Test::Unit test, you begin by requiring test/unit, which is part of Ruby’s standard library. This provides the Test::Unit::TestCase class inherited from on the next line. Inheriting from this class provides the functionality to run any method defined in this class whose name begins with test. Additionally, you can define tests by using the test method:

C++
test "truth" do
  assert true
end

To run this file, you run ruby example_test.rb in the terminal. When this command completes, you see some output, the most relevant being two of the lines in the middle:

.
1 tests, 1 assertions, 0 failures, 0 errors, 0 skips

The first line is a singular period. This is Test::Unit’s way of indicating that it ran a test and the test passed. If the test had failed, it would show up as an F; if it had errored, an E. The second line provides statistics on what happened, specifically that there was one test and one assertion, and that nothing failed, there were no errors, and nothing was skipped. Great success!

The assert method in your test makes an assertion that the argument passed to it evaluates to true. This test passes given anything that’s not nil or false. When this method fails, it fails the test and raises an exception. Go ahead, try putting 1 there instead of true. It still works:

.
1 tests, 1 assertions, 0 failures, 0 errors, 0 skips

In the following listing, you remove the test_ from the beginning of your method and define it as simply a truth method.

Listing 2 example/example_test.rb, alternate truth test

def truth
  assert true
end

Test::Unit tells you there were no tests specified by running the default_test method internal to Test::Unit:

No tests were specified.
1 tests, 1 assertions, 1 failures, 0 errors

Remember to always prefix Test::Unit methods with test!

Saving Bacon

Let’s make this a little more complex by creating a bacon_test.rb file and writing the test shown in the following listing.

Listing 3 example/bacon_test.rb

C++
require 'test/unit'
class BaconTest < Test::Unit::TestCase
  def test_saved
    assert Bacon.saved?
  end
end

Of course, you want to ensure that your bacon[2] is always saved, and this is how you do it. If you now run the command to run this file, ruby bacon_test.rb, you get an error:

C++
NameError: uninitialized constant BaconTest::Bacon

Your test is looking for a constant called Bacon and cannot find it because you haven’t yet defined the constant. For this test, the constant you want to define is a Bacon class.

You can define this new class before or after the test. Note that in Ruby you usually must define constants and variables before you use them. In Test::Unit tests, the code is only run when it finishes evaluating it, which means you can define the Bacon class after the test. In the next listing, you follow the more conventional method of defining the class above the test.

Listing 4 example/bacon_test.rb

C++
require 'test/unit'
class Bacon

end
class BaconTest < Test::Unit::TestCase
  def test_saved
    assert Bacon.saved?
  end
end

Upon rerunning the test, you get a different error:

C++
NoMethodError: undefined method `saved?' for Bacon:Class

Progress! It recognizes there’s now a Bacon class, but there’s no saved? method for this class, so you must define one, as in the following listing.

Listing 5 example/bacon_test.rb

class Bacon
  def self.saved?
    true
  end
end

One more run of ruby bacon_test.rb and you can see that the test is now passing:

.
1 tests, 1 assertions, 0 failures, 0 errors, 0 skips

Your bacon is indeed saved! Now any time that you want to check if it’s saved, you can run this file. If somebody else comes along and changes that true value to a false, then the test will fail:

F
  1) Failure:
test_saved(BaconTest) [bacon_test.rb:11]:
Failed assertion, no message given.

Test::Unit reports "Failed assertion, no message given" when an assertion fails. You should probably make that error message clearer! To do so, you can specify an additional argument to the assert method in your test, like this:

assert Bacon.saved?, "Our bacon was not saved :("

Now when you run the test, you get a clearer error message:

C++
  1) Failure:
test_saved(BaconTest) [bacon_test.rb:11]:
Our bacon was not saved :(

Summary

You’ve just seen the basics of TDD using Test::Unit. It’s handy to know because it establishes the basis for TDD in Ruby. Test::Unit is also the default testing framework for Rails, so you may see it around in your travels.

Here are some other Manning titles you might be interested in:

image003.jpg

Grails in Action
Glen Smith and Peter Ledbrook

image004.jpg

Griffon in Action
Andres Almiray and Danno Ferrin

image005.jpg

Spring in Action, Third Edition
Craig Walls

[1] Such as the one GitHub has made: http://github.com/blog/653-our-new-build-status-indicator.

[2] Both the metaphorical and the crispy kinds.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)