oreilly.comSafari Books Online.Conferences.


Unit Testing Your Documentation
Pages: 1, 2, 3, 4

Scripting an irb Session

Now the program has a list of Assertion objects: chunks of code and the expected values when that code is run. We wrote the Ruby Cookbook code to run in an irb session, so that's how I tested it. I decided it would be easier to script an irb session than to figure out how to get the equivalent behavior out of eval.

The hardest part of scripting an irb session is knowing what to override. The class to modify is IRB::Irb. To get an Irb instance to accept an alternate source of input, give it an input class that supports the methods gets and prompt=.

Comparing the actual result of an expression to the expected result is also easy. The value of the most recent expression in an irb session is available through the Irb object's instance variable @context.

Here's an Irb subclass that takes a harness variable as its input source and sets up the interpreter to use it. The irb code passes HarnessedIrb#output_value the value of every expression it runs. To keep this class simple, its output_value implementation simply delegates to the harness class. This class and an appropriate harness are all you need to instrument an irb session and inspect the output of the expressions.

require 'irb'
class HarnessedIrb < IRB::Irb

  def initialize(harness)
    # Prevent Ruby code from being echoed to standard output.
    IRB.conf[:VERBOSE] = false
    @harness = harness
    super(nil, harness)

  def output_value

  def run
    IRB.conf[:MAIN_CONTEXT] = self.context

Here's an AssertionTestingHarness class that takes a list of Assertion objects and feeds the code into irb, one line at a time.

@require 'stringio'

class AssertionTestingHarness
  def initialize(assertions)
    @assertions = assertions
    @assertion_counter, @line_counter = 0
    @keep_feeding = false
    $stdout =

  # Called when irb wants a line of input.
  def gets
    line = nil
    assertion = @assertions[@assertion_counter]
    @line_counter += 1 if @keep_feeding
    line = assertion[:code][@line_counter] + "\n" if assertion
    @keep_feeding = true
    return line

  # Called by irb to display a prompt to the end-user. We have no
  # end-user, and so no prompt. Strangely, irb knows that we have no
  # prompt, but it calls this method anyway.
  def prompt=(x)

The irb interpreter calls output_value every time it evaluates a line of code, but nothing happens except on the final line of a code chunk, when it's time to test the assertion.

  # Compare a value received by irb to the expected value of the
  # current assertion.
  def output_value(value)
      assertion = @assertions[@assertion_counter]
      if @line_counter < assertion[:code].size - 1
        # We have more lines of code to run before we can test an assertion.
        @line_counter += 1

The interpreter passes the result of the Ruby expression as an argument to output_value. If the assertion is a :value-type assertion, the harness simply compares the expected value to that argument.

If the assertion is a :stdout-type, then it is ignorable. Instead, the harness captures the standard output gathered during the code chunk, and compares that to the expected value. This is why the initialize method replaces $stdout with a StringIO object.

        # We're done with this code chunk; it's time to check its assertion.
        value = value.inspect
        if assertion[:how] == :value
          # Compare expected to actual expression value
          actual = value.strip
          # Compare expected to actual standard output.
          actual = $stdout.string.strip
        report_assertion(assertion, actual)
        # Reset standard output and move on to the next code chunk
        @assertion_counter += 1
        @line_counter = 0
        $stdout.string = ""
    rescue Exception => e
      # Restore standard output and re-throw the exception.
      $stdout = STDOUT
      raise e
    @keep_feeding = false

The report_assertion method compares an assertion to reality. When testing the book, my harness printed out an HTML report for each recipe, flagging failed assertions in red (here's the report for "Word-Wrapping Lines of Text"). The implementation presented here is much simpler; it inspects the assertion in light of the code chunk's actual value. A third implementation might make a Test::Unit assertion here.

  # Compare the expected value of an assertion to the actual value.
  def report_assertion(assertion, actual)
    STDOUT.puts assertion.inspect(actual)

Finally, here is code to test standard input when running this code as a script.

if $0 == __FILE__
  assertions =$

Run a recipe into this script to extract, evaluate, and test its code listings. The following is the result of this script run against the sample code from "Word-Wrapping Lines of Text" given above. There are five lines of Ruby code here and two assertions:

*** When I run this code: ***
def wrap(s, width=78)
  s.gsub(/(.{1,#{width}})(\s+|\Z)/, "\\1\n")
wrap("This text is too short to be wrapped.")
*** I expect this value: ***
"This text is too short to be wrapped.\n"
*** What I get is this: ***
"This text is too short to be wrapped.\n"

*** When I run this code: ***
puts wrap("This text is not too short to be wrapped.", 20)
*** I expect this stdout: ***
This text is not too
short to be wrapped.
*** What I get is this: ***
This text is not too
short to be wrapped.

Pages: 1, 2, 3, 4

Next Pagearrow

Sponsored by: