How to Use ZenTest with Ruby
Refactoring and unit testing are a great pair of tools and should be a part of every programmer's workbench. Sadly, not every programmer knows how to use these tools. My first exposure to them came when I started using Ruby. Refactoring and unit testing are a big part of the landscape in the Ruby community.
Some time ago, I translated the refactoring example from the first chapter of Martin Fowler's excellent book Refactoring from Java to Ruby. I felt this would be a great way to learn more about refactoring and brush up on my Ruby while I was at it. Recently, I decided to update the translation for Ruby 1.8.X. One of the things I needed to do was convert the old unit tests to work with Test::Unit, the new unit testing framework for Ruby. I wasn't looking forward to building a new test suite, however. Fortunately, help was available.
Ryan Davis has written a great tool called ZenTest, which creates test suites for existing bodies of code. Because a lot of people are new to refactoring, unit testing and ZenTest, this article serves as an introduction to this trio of tools.
Martin's example code is built around a video store application. His original code includes three classes--Customer, Movie and Rental. I focus on only the Customer class in this article. Here's the original code:
class Customer
attr_accessor :name
def initialize(name)
@name = name
@rentals = Array.new
end
def addRental(aRental)
@rentals.push(aRental)
end
def statement
totalAmount = 0.0
frequentRenterPoints = 0
rentals = @rentals.length
result = "\nRental Record for #{@name}\n"
thisAmount = 0.0
@rentals.each do |rental|
# determine amounts for each line
case rental.aMovie.pricecode
when Movie::REGULAR
thisAmount += 2
if rental.daysRented > 2
thisAmount += (rental.daysRented - 2) * 1.5
end
when Movie::NEW_RELEASE
thisAmount += rental.daysRented * 3
when Movie::CHILDRENS
thisAmount += 1.5
if each.daysRented > 3
thisAmount += (rental.daysRented - 3) * 1.5
end
end
# add frequent renter points
frequentRenterPoints += 1
# add bonus for a two day new release rental
if ( rental.daysRented > 1) &&
(Movie::NEW_RELEASE == rental.aMovie.pricecode)
frequentRenterPoints += 1
end
# show figures for this rental
result +="\t#{rental.aMovie.title}\t#{thisAmount}\n"
totalAmount += thisAmount
end
result += "Amount owed is #{totalAmount}\n"
result += "You earned #{frequentRenterPoints} frequent renter points"
end
end
It's not the cleanest code in the world, but that's the point--this represents the code as you get it from the user. It contains no tests and is poorly laid out, but it works. Your your job is to make it work better without breaking it. So, where to start? With unit tests, of course. Time to grab ZenTest.
You can tell ZenTestit to do this:
$ zentest videostore.rb > test_videostore.rb
which produces a file full of tests. Running the test suite doesn't do exactly what we were hoping, however:
$ ruby testVideoStore.rb Loaded suite testVideoStore
Started
EEEEEEEEEEE
Finished in 0.008974 seconds.
1) Error!!!
test_addRental(TestCustomer):
NotImplementedError: Need to write test_addRental
testVideoStore.rb:11:in `test_addRental'
testVideoStore.rb:54
2) Error!!!
test_name=(TestCustomer):
NotImplementedError: Need to write test_name=
testVideoStore.rb:15:in `test_name='
testVideoStore.rb:54
3) Error!!!
test_statement=(TestCustomer):
NotImplementedError: Need to write test_statement
testVideoStore.rb:19:in `test_statement'
testVideoStore.rb:54
.
.
.
11 tests, 0 assertions, 0 failures, 11 errors
$
What exactly did we get out of running ZenTest like this? Here's the portion of our new test suite that matters for the Customer class:
# Code Generated by ZenTest v. 2.1.2
# classname: asrt / meth = ratio%
# Customer: 0 / 3 = 0.00%
require 'test/unit'
class TestCustomer < Test::Unit::TestCase
def test_addRental
raise NotImplementedError, 'Need to write test_addRental'
end
def test_name=
raise NotImplementedError, 'Need to write test_name='
end
def test_statement
raise NotImplementedError, 'Need to write test_statement'
end
end
ZenTest built three test methods: one for the accessor method, one for the addRental method and one for the statement method. Why is there nothing for the initializer? This is skipped because initializers tend to be pretty bulletproof. If they're not, it's easy to add the test method yourself. Besides, we'll be testing it indirectly when we write test_name=, the tests for the accessor method. We need to add one other thing--the test suite doesn't load the code we're testing. Changing the beginning of the script to require the videostore.rb file should do the trick for us.
# Code Generated by ZenTest v. 2.1.2
# classname: asrt / meth = ratio%
# Customer: 0 / 3 = 0.00%
require 'test/unit'
require 'videostore'
The little snippet of comments at the top lets us know we have three methods under test in the Customer class, zero assertions testing them and no coverage. Let's fix that. We start by writing some tests for test_name=. It doesn't matter what order we go in here, test_name= simply is a convenient place to start.
def test_name=
aCustomer = Customer.new("Fred Jones")
assert_equal("Fred Jones",aCustomer.name)
aCustomer.name = "Freddy Jones"
assert_equal("Freddy Jones",aCustomer.name
end
Running testVideoStore.rb again gives us:
$ ruby testVideoStore.rb
Loaded suite testVideoStore
Started
E.EEEEEEEEE
Finished in 0.011233 seconds.
1) Error!!!
test_addRental(TestCustomer):
NotImplementedError: Need to write test_addRental
testVideoStore.rb:13:in `test_addRental'
testVideoStore.rb:58
2) Error!!!
test_statement(TestCustomer):
NotImplementedError: Need to write test_statement
testVideoStore.rb:23:in `test_statement'
testVideoStore.rb:58
.
.
.
11 tests, 2 assertions, 0 failures, 10 errors
$
So far, so good. The line of Es, which shows errors in the test run, has been reduced by one, and the summary line at the bottom tells us roughly the same thing.
We don't have a way to test addRental directly, so we simply write a stub test for now.
def test_addRental
assert(1) # stub test, since there is nothing in the method to test
end
When we run the tests again, we get:
$ ruby testVideoStore.rb
Loaded suite testVideoStore
Started
..EEEEEEEEE
Finished in 0.008682 seconds.
1) Error!!!
test_statement(TestCustomer):
NotImplementedError: Need to write test_statement
testVideoStore.rb:22:in `test_statement'
testVideoStore.rb:57
.
.
.
11 tests, 3 assertions, 0 failures, 9 errors
$
We're doing better and better; there's only one error left in the TestCustomer class. Let's finish up with a test that clears our test_statement error and verifies that addRental works correctly:
def test_statement
aMovie = Movie.new("Legacy",0)
aRental = Rental.new(aMovie,2)
aCustomer = Customer.new("Fred Jones")
aCustomer.addRental(aRental)
aStatement = "\nRental Record for Fred Jones\n\tLegacy\t2.0
Amount owed is 2.0\nYou earned 1 frequent renter points"
assert_equal(aStatement,aCustomer.statement)
end
We run the tests again and see:
$ ruby testVideoStore.rb
Loaded suite testVideoStore
Started
...EEEEEEEE
Finished in 0.009378 seconds.
.
.
.
11 tests, 4 assertions, 0 failures, 8 errors
$
Great! The remaining errors are occurring on the Movie and Rental classes; the Customer class is clean.
We can continue along like this for the remaining classes, but I'm not going to bore you with those details. Instead, I'd like to look at how ZenTest can help when you've already got some tests in place. Later development allows us to do exactly. Say, for example, the video store owner wants a new Web-based statement that is accessible to customers on-line. After a bit of refactoring and new development, the code looks like this:
class Customer
attr_accessor :name
def initialize(name)
@name = name
@rentals = Array.new
end
def addRental(aRental)
@rentals.push(aRental)
end
def statement
result = "\nRental Record for #{@name}\n"
@rentals.each do
|each|
# show figures for this rental
result +="\t#{each.aMovie.title}\t#{each.getCharge}\n"
end
result += "Amount owed is #{getTotalCharge}\n"
result +=
"You earned #{getFrequentRenterPoints} frequent renter points"
end
def htmlStatement
result = "\n<H1>Rentals for <EM>#{name}</EM></H1><P>\n"
@rentals.each do
|each|
result += "#{each.aMovie.title}: #{each.getCharge}<BR>\n"
end
result += "You owe <EM>#{getTotalCharge}</EM><P>\n"
result +=
"On this rental you earned <EM>#{getFrequentRenterPoints}" +
"</EM> frequent renter points<P>"
end
def getTotalCharge
result = 0.0
@rentals.each do
|each|
result += each.getCharge()
end
result
end
def getFrequentRenterPoints
result = 0
@rentals.each do
|each|
result += each.getFrequentRenterPoints
end
result
end
end
There's a lot of new stuff in the code now. If we run ZenTest again, it would pick up the methods on which we don't have any coverage. We should have written them as we wrote the new methods, but this method is a bit more illustrative. So this time, we invoke ZenTest a little bit differently:
$ zentest videostore.rb testVideoStore.rb > Missing_tests
and our (trimmed) output looks like this:
# Code Generated by ZenTest v. 2.1.2
# classname: asrt / meth = ratio%
# Customer: 4 / 6 = 66.67%
require 'test/unit'
class TestCustomer < Test::Unit::TestCase
def test_getFrequentRenterPoints
raise NotImplementedError,
'Need to write test_getFrequentRenterPoints'
end
def test_getTotalCharge
raise NotImplementedError, 'Need to write test_getTotalCharge'
end
def test_htmlStatement
raise NotImplementedError, 'Need to write test_htmlStatement'
end
end
We need to fill in three more test methods to get our complete coverage. As we write these, we can migrate them to our existing testVideoStore.rb test suite. Then, we can keep moving ahead with refactoring and adding new features. In the future, of course, we simply will add tests as we go along. ZenTest can help here, too. We can write stubs for new development and then run ZenTest to create the new test stubs as well. After some refactorings, such as extract method, ZenTest can be used in the same way.
Refactoring and unit testing are powerful tools for programmers, and ZenTest provides an easy way to start using them in a Ruby environment. Hopefully, this introduction has whetted your appetite.
-- -pate http://on-ruby.blogspot.com
Realizing the promise of Apache® Hadoop® requires the effective deployment of compute, memory, storage and networking to achieve optimal results. With its flexibility and multitude of options, it is easy to over or under provision the server infrastructure, resulting in poor performance and high TCO. Join us for an in depth, technical discussion with industry experts from leading Hadoop and server companies who will provide insights into the key considerations for designing and deploying an optimal Hadoop cluster.
Sponsored by AMD
Built-in forensics, incident response, and security with Red Hat Enterprise Linux 6
Every security policy provides guidance and requirements for ensuring adequate protection of information and data, as well as high-level technical and administrative security requirements for a system in a given environment. Traditionally, providing security for a system focuses on the confidentiality of the information on it. However, protecting the data integrity and system and data availability is just as important. For example, when processing United States intelligence information, there are three attributes that require protection: confidentiality, integrity, and availability.
Learn more about catching the bad guy in this free white paper.
Sponsored by DLT Solutions
| Designing Electronics with Linux | May 22, 2013 |
| Dynamic DNS—an Object Lesson in Problem Solving | May 21, 2013 |
| Using Salt Stack and Vagrant for Drupal Development | May 20, 2013 |
| Making Linux and Android Get Along (It's Not as Hard as It Sounds) | May 16, 2013 |
| Drupal Is a Framework: Why Everyone Needs to Understand This | May 15, 2013 |
| Home, My Backup Data Center | May 13, 2013 |
- seo services in india
1 hour 29 min ago - For KDE install kio-mtp
1 hour 30 min ago - Evernote is much more...
3 hours 30 min ago - Reply to comment | Linux Journal
12 hours 16 min ago - Dynamic DNS
12 hours 50 min ago - Reply to comment | Linux Journal
13 hours 48 min ago - Reply to comment | Linux Journal
14 hours 38 min ago - Not free anymore
18 hours 40 min ago - Great
22 hours 28 min ago - Reply to comment | Linux Journal
22 hours 36 min ago
Enter to Win an Adafruit Pi Cobbler Breakout Kit for Raspberry Pi

It's Raspberry Pi month at Linux Journal. Each week in May, Adafruit will be giving away a Pi-related prize to a lucky, randomly drawn LJ reader. Winners will be announced weekly.
Fill out the fields below to enter to win this week's prize-- a Pi Cobbler Breakout Kit for Raspberry Pi.
Congratulations to our winners so far:
- 5-8-13, Pi Starter Pack: Jack Davis
- 5-15-13, Pi Model B 512MB RAM: Patrick Dunn
- 5-21-13, Prototyping Pi Plate Kit: Philip Kirby
- Next winner announced on 5-27-13!
Featured Jobs
| Linux Systems Administrator | Houston and Austin, Texas | Host Gator |
| Senior Perl Developer | Austin, Texas | Host Gator |
| Technical Support Rep | Houston and Austin, Texas | Host Gator |
| UX Designer | Austin, Texas | Host Gator |
| Web & UI Developer (JavaScript & j Query) | Austin, Texas | Host Gator |
Free Webinar: Hadoop
How to Build an Optimal Hadoop Cluster to Store and Maintain Unlimited Amounts of Data Using Microservers
Realizing the promise of Apache® Hadoop® requires the effective deployment of compute, memory, storage and networking to achieve optimal results. With its flexibility and multitude of options, it is easy to over or under provision the server infrastructure, resulting in poor performance and high TCO. Join us for an in depth, technical discussion with industry experts from leading Hadoop and server companies who will provide insights into the key considerations for designing and deploying an optimal Hadoop cluster.
Some of key questions to be discussed are:
- What is the “typical” Hadoop cluster and what should be installed on the different machine types?
- Why should you consider the typical workload patterns when making your hardware decisions?
- Are all microservers created equal for Hadoop deployments?
- How do I plan for expansion if I require more compute, memory, storage or networking?



Comments
Concatenate strings with << instead
Avoid concatenating strings using += because it duplicates the receiver string that will remain unused in memory until the GC starts. You should rather use << that modifies the receiver by appending the given string to it. It's also quite faster.
... continuation of the above comment discussing the << operator
(sorry for the truncated comment above) ... You should rather use the double "lower than" operator (cannot be written here or the comment gets cut) that modifies the receiver appending the given string to it and returns the string to allow chained calls. It's also quite faster.
Note that Ryan has moved Zentest to RubyForge...
....right here.
Yours,
Tom
Re: How to Use ZenTest with Ruby
I don't think assert(1) is a good thing. That test should fail, imo. Maybe raising something like NotImplementedError would be better.
A pleasant article, anyway, now I'm wondering how cool an integration of ZenTest with FreeRIDE (and the included Refactoring-Browser plugin) could be..
Re: How to Use ZenTest with Ruby
Great idea for another article or two!
Re: How to Use ZenTest with Ruby
ooh -- ZenTest, rrb, and FreeRIDE together? that sounds really good! Rich, Ryan? are you guys listening?
Re: How to Use ZenTest with Ruby
you can try it yourself :)
I wrote a simple test generation plugin long time ago (see DumbTestBuilder on the FR wiki) in an hour, and I had never used the FreeRIDE/FreeBASE api before..
Re: How to Use ZenTest with Ruby
the assert(1) test set my alarm bells ringing, but I see your point...
Test::Unit is Another Good Reason To Use Ruby, but I always find myself copying an existing testsuite and editing because I cant' remember syntax worth a damn, so anything that bootstraps a test*rb creation is worth its weight in gold..
Re: How to Use ZenTest with Ruby
Excellent. I always wondered what was the easiest way of adding unit tests and what you show here is really simple and straightforward. I love it.
Re: How to Use ZenTest with Ruby
Hey, that's excellent! Thanks for the article.
--Gavin Sinclair
Re: How to Use ZenTest with Ruby
Never used ZenTest before, but it seems like a good way to get started with adding unit testing to those old project I started before I learned how to write decently tested code.
I always like helpers that save me a step or two.
Re: How to Use ZenTest with Ruby
I had never looked into ZenTest, but now that I realize it creates Test::Unit tests for you, it does sound like a great way to deal with legacy (i.e., "not written test-first", in TDD parlance) code.
Re: How to Use ZenTest with Ruby
ZenTest is bidirectional, so it is also a great way to do test-first. Write the tests, run them... throw exceptions galore because the classes referenced do not exist. ZenTest will fill in those blanks for you as well.
Re: How to Use ZenTest with Ruby
Very cool article. I often scan over the code with my eyes and see what I am missing and try to do the refactoring manually. I had never heard of Zentest before this article. I will have to check it out....it looks like it'll make my life easier and less error prone. ;)
Re: How to Use ZenTest with Ruby
Wow! ZenTest looks like a really cool way to generate the unit testing scaffolding code. Looks like it takes the tedium out of unit testing.
Very informative.
It would be great to see more Ruby articles like this as well.
thanks for great article,
thanks for great article, will try zentest
just to point out typo, in last assertion here, no closing paren
def test_name=
aCustomer = Customer.new("Fred Jones")
assert_equal("Fred Jones",aCustomer.name)
aCustomer.name = "Freddy Jones"
assert_equal("Freddy Jones",aCustomer.name
end
can testers use this for functional testing
can testers use this for functional testing?.
(or)
Developers can use it...
pls let me know.if tester can use this, what are the things he wants to know