RubyLearning

Helping Ruby Programmers become Awesome!

Do you know Ruby Doctest?

October 5, 2011 | By Victor Goff

This guest post is by Victor Goff, who enjoys mentoring Ruby at RubyLearning.org since 2008. He also blogs occasionally at vgoff.posterous.com.

Rubydoctest is a gem that we will be installing. The purpose of the gem is simply to provide a way to document our programs using IRB sessions, resulting in a way to provide usage examples as well as knowing when an expected use fails because of some update or changes to our program or environment.

Installing Ruby DocTest

Rubydoctest can be installed by doing the following from the command line:

$ gem install rubydoctest

If you are running Windows or using RVM the above should work (pending network/internet connectivity) but you may need to use sudo to install depending on your set up.

The parts of Ruby DocTest

Rubydoctest information can be contained within a comment or a comment block.

Let's start by creating a file hello.rb:

def hello name
  "Hello " + name
end
=begin # This is the beginning of a comment block
doctest: hello " World" will return "Hello World"
>> hello "World"
=> "Hello World"
=end

We have our trusty method called hello, but we have added a comment block with doctest information inside. Other than the doctest: directive, it should remind you of an IRB session.

The doctest: directive is the human readable title of the test. If this isn't supplied you would end up seeing 'default' as the title.

To use rubydoctest:

USAGE: rubydoctest [options]

  rubydoctest parses Ruby files (.rb) or DocTest files (.doctest) for irb-style
  sessions in comments, and runs the commented sessions as tests.

  Options:
    Output Format:
      --html  - output in HTML format
      --plain - force output in plain text (no Ansi colors)

    Debug:
      --ignore-interactive - do not heed !!! special directives
      --trace     - turn backtrace on to debug Ruby DocTest
      --debugger  - include ruby-debug library / gem

Let's go ahead and run the following command to see what happens here:

rubydoctest hello.rb
=== Testing 'hello.rb'...
 OK  | hello "World" will return "Hello World"
1 comparisons, 1 doctests, 0 failures, 0 errors

As you can see, we get passing tests. And if this were above the method definition it may even be collected during ri and rdoc generation (this process is outside the scope of this article.)

Refactoring with RubyDoctest

Once we have a simple test in place, and that test is passing, we can refactor it, with confidence that the method still passes.

I noticed that I used "String" + variable in this method, but realized that it may read a little better (and create only one object) if I use string interpolation:

def hello name
  "Hello #{name}"
end

Which I think should pass the test, but just for good measure, we want to actually test it:

rubydoctest hello.rb
=== Testing 'hello.rb'...
 OK  | hello "World" will return "Hello World"
1 comparisons, 1 doctests, 0 failures, 0 errors

And as we can see, it passes. Next, we will fully develop our hello method so that it has some nice features.

Using RubyDoctest in a TDD manner

We will be starting our hello method over from scratch. Going through the full life cycle of a small method, and testing along the way. It will be conversational, and demonstrative, so that you can follow along as we go.

Starting with an idea

It all starts with an idea: I would like a hello method that will simply state "Hello World!" In order to make this happen, and have the documentation and the tests that I would eventually like to have, I place this in my hello.rb file:

=begin
doctest: hello returns "Hello World!"
>> hello
=> "Hello World!"
=end

And to make sure that it is written correctly, we do want to run the following command and get the information back from our test:

rubydoctest hello.rb
=== Testing 'hello.rb'...
ERR  | hello returns "Hello World!"
       NameError: undefined local variable or method `hello' for main:Object
         from hello.rb:3
       hello
1 comparisons, 1 doctests, 0 failures, 1 errors

This information, if we pause and read it, tells us exactly what we need to get it past this error. It states "undefined local variable or method 'hello'" and from which line it was called. We have already made the assumption that we want a method, and so we can do the least amount to cause this ERR to no longer exist.

So let's write the least amount of code that we can think of to get it to not error:

def hello
end

Once I save this, and run the rubydoctest command, I get the following:

rubydoctest hello.rb
=== Testing 'hello.rb'...
FAIL | hello returns "Hello World!"
       Got: nil
       Expected: "Hello World!"
         from hello.rb:4
1 comparisons, 1 doctests, 1 failures, 0 errors

The error count is 0, which means that we have progressed past the yellow ERR message. We will generally always want to work past ERR messages first, then FAIL messages, until we get to OK.

Let's go forward in our journey and get past this FAIL message. I think it is fairly obvious that we can simply have the method return the string "Hello World!" and get this to pass:

def hello
  "Hello World!"
end

Simplistic and rids us of the failure in our test:

rubydoctest hello.rb
=== Testing 'hello.rb'...
 OK  | hello returns "Hello World!"
1 comparisons, 1 doctests, 0 failures, 0 errors

It continues with expanding ideas

Let's think about what we have. A hello method that greets the world. Not too bad. But let's say that we want to be able to use it to greet the world, but also to greet someone in particular. Continuing our program documentation, we add the following:

doctest: hello "reader" returns "Hello reader!"
>> hello "student"
=> "Hello student!"

Those three lines should be added to comment block and our entire file looks like so:

=begin
doctest: hello returns "Hello World!"
>> hello
=> "Hello World!"
doctest: hello "reader" returns "Hello reader!"
>> hello "student"
=> "Hello student!"
=end
def hello
  "Hello World!"
end

When we run the doctest (which we should do any time we make a change in either the documentation or in the code itself), we should get:

rubydoctest hello.rb
=== Testing 'hello.rb'...
 OK  | hello returns "Hello World!"
ERR  | hello "reader" returns "Hello reader!"
       ArgumentError: wrong number of arguments (1 for 0)
         from hello.rb:6
       hello "student"
2 comparisons, 2 doctests, 0 failures, 1 errors

From this we can see that the current way we want to use it by giving one argument is in error on line 6, because our method wants 0. That is what the "(1 for 0)" means.

How might we get out of our error condition? Perhaps like this?

def hello name
  "Hello World!"
end

Notice how we added the argument of name to our method. As we have made a change, it is time to run rubydoctest again:

rubydoctest hello.rb
=== Testing 'hello.rb'...
ERR  | hello returns "Hello World!"
       ArgumentError: wrong number of arguments (0 for 1)
         from hello.rb:3
       hello
FAIL | hello "reader" returns "Hello reader!"
       Got: "Hello World!"
       Expected: "Hello student!"
         from hello.rb:7
2 comparisons, 2 doctests, 1 failures, 1 errors

As you can see, we went from ERR to FAIL, which is where we want to be. Ignoring the first test, we go from FAIL to OK (pass) in the simplest change we can think of:

def hello name
  "Hello student!"
end

Now we can look for any ERR messages as they are critical. Changing the method itself to accept 0 or more arguments with a default value is the right thing to do here:

def hello name='World'
  "Hello " + name + "!"
end

That gets us out of fail and into OK (pass) for each test:

rubydoctest hello.rb
=== Testing 'hello.rb'...
 OK  | hello returns "Hello World!"
 OK  | hello "reader" returns "Hello reader!"
2 comparisons, 2 doctests, 0 failures, 0 errors

Refactoring in the Green

You might think we are done. But we are in 'green' mode and so are free to refactor our code. Let's use string interpolation which makes it more readable and creates only one String object:

def hello name='World'
  "Hello #{name}!"
end

And of course, we run rubydoctest again to make sure we didn't break anything, and we see that we are good.

I hope you found this article valuable. Feel free to ask questions and give feedback in the comments section of this post. Thanks!

Tags: Ruby DocTest Programming Ruby programming