File: how_to_use_zentest.txt [ZenTest-4.11.0 Documentation]

### Home Classes Methods

### Files

- History.txt
- Manifest.txt
- README.txt
- how_to_use_zentest.txt
- example.txt

### Class Index

Quicksearch

- Autotest
- Autotest::AutoUpdate
- Autotest::Bundler
- Autotest::Isolate
- Autotest::Once
- Autotest::Preload
- Autotest::RCov
- Autotest::Restart
- Autotest::Timestamp
- FunctionalTestMatrix
- Module
- Multiruby
- UnitDiff
- ZenTest
- ZenTestMapping

No matching classes.

How to Use ZenTest with Ruby by Pat Eyler <pate@kohalabs.com> linuxjournal.com/article.php?sid=7776 (included in this package with permission)

Refactoring and unit testing are a great pair of tools for every programmer's workbench. Sadly, not every programmer knows how to use them. 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, out of Java and into 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 change was to convert the old unit tests to work with Test::Unit, the new unit testing framework for Ruby.

I wasn't really looking forward to building a new test suite though. Fortunately, help was available. Ryan Davis has written a great tool called ZenTest, which creates test suites for existing bodies of code. Since a lot of people are new to refactoring, unit testing, and ZenTest, I thought this would be a great chance to introduce you to this trio of tools.

Martin's example code is built around a video store application. In his original code, there are three classes; Customer, Movie, and Rental. I'll focus on just 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

Not the cleanest code in the world, but it is supposed to be that way. This represents the code as you get it from the user. No tests, poorly laid out, but working -- and it's your job to make it better without breaking it. So, where to start? With unit tests of course.

Time to grab ZenTest. You can run it like this:

    $ zentest videostore.rb > test_videostore.rb

which produces a file full of tests. Running the test suite doesn't do quite what we were hoping though:

    $ 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
    $

So what exactly did we get out of 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 nothing for the initializer? Well, initializers tend to be pretty bulletproof (if they're not, it's pretty 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). There's one other thing we'll need to add, the test suite doesn't load the code we're testing. Changing the beginning of the script to require the videostore.rb file will 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'

That little snippet of comments at the top lets us know that we have three methods under test in the Customer class, zero assertions testing them, and no coverage. Let's fix that. We'll start by writing some tests for test_name= (no, it really doesn't matter what order we go in -- this is just 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 'E's (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 really don't have a way to test addRental directly, so we'll just write an 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
    $

Better and better, just one error left in the TestCustomer class. Let's finish up with a test that will clear our test_statement error and verify 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 only errors left are on the Movie and Rental classes, the Customer class is clean.

We can continue along like this for the remaining classes, but I'll not 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 just that -- the video store owner wants a new web based statement for web using customers.

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 here. If we run ZenTest again, it'll pick up the methods we don't have any coverage on (we should have written them as we wrote the new methods, but this is a bit more illustrative). This time, we'll 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

Hmmm, three more test methods to fill in to get our complete coverage. As we write these, we can just migrate them into our existing testVideoStore.rb test suite. Then we can keep moving ahead with refactoring and adding new features. In the future, let's just be sure we add tests as we go along. ZenTest can help you here too. You can write stubs for new development, then run ZenTest to create your new test stubs as well. After some refactorings (like 'extract method'), ZenTest can be used 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.

If you're interested in learning more about refactoring, please grab a copy of 'Refactoring: Improving the Design of Existing Code' and take a look at www.refactoring.com. For more information about unit testing, please see: c2.com/cgi/wiki?UnitTest, www.junit.org/index.htm, and www.extremeprogramming.org/rules/unittests.html.

The latest information about Test::Unit and ZenTest are available at their home pages: testunit.talbott.ws (for Test::Unit) and www.zenspider.com/ZSS/Products/ZenTest.

[Validate]

Generated with the Darkfish Rdoc Generator 2.