| Rails 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
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:
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
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:
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
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:
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:
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:
[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.