oreilly.comSafari Books Online.Conferences.


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

Parsing a Recipe into Assertions

The rest of this article presents test_recipe.rb, a modified version of the Ruby script I used to parse and test our recipes' assertions. It starts with a simple struct class to hold chunks of code and the associated assertions:

# test_recipe.rb

Assertion =, :should_give, :how)
class Assertion
  SEPARATOR = "\n#{'-' * 50}\n"

  def inspect(against_actual_value=nil)
    s = "*** When I run this code: ***" +
      SEPARATOR + code.join("\n") + SEPARATOR +

      "*** I expect this #{how}: ***" +
      SEPARATOR + should_give.join("\n") + SEPARATOR

    if against_actual_value
      s += "*** What I get is this: ***" +
        SEPARATOR + against_actual_value + SEPARATOR + "\n"

Every recipe is treatable as a self-contained file. The AssertionParser class transforms one such file into an array of Assertion objects representing data to be fed into an irb session.

It starts by splitting a recipe on the triple backtick and examining each snippet of code. In the book, most of these snippets are part of the recipe's irb session, but some are sample shell sessions, standalone Ruby files, or code in a language other than Ruby. The program needs to filter out those snippets. For simplicity's sake I omitted that code, which is just lots of checks against the first few bytes of a snippet.

# Parses a Ruby Cookbook-formatted recipe into a set of assertions
# about chunks of code.
class AssertionParser
  attr_reader :assertions


  def initialize(code)
    @assertions = []

    # Strip out the code snippets from the English text.
    snippets = []
    code.split(/```\s*\n/).each_with_index do |x, i|
      # Not shown: filter snippets that aren't part of the irb session.
      snippets << x if (i % 2 == 1)

The second step is to separate the Ruby code into chunks, each of which terminates in an assertion to check. AssertionParser scans the Ruby code line by line, gathering chunks of code, finding each assertion and associating it with the foregoing chunk.

This section handles a line containing an assertion about an expression's expected standard output:

    # Divide the code into assertions.
    snippets.join("\n").each do |loc|
      if loc.size > 0
        if EXPRESSION_OUTPUT_COMMENT.match(loc)
          # The code so far is supposed to write to standard output.
          # The expected output begins on this line and may continue
          # in subsequent lines.
 = :stdout if @assertion.should_give.empty?

          # Get rid of the comment symbol, leaving only the expected output.
          loc.sub!(EXPRESSION_OUTPUT_COMMENT, '')
          @assertion.should_give << loc

Another section handles a line containing an assertion about an expression's expected value:

        elsif EXPRESSION_VALUE_COMMENT.match(loc)
          # The Ruby expression on this line is supposed to have a
          # certain value. If there is no expression on this line,
          # then the expression on the previous line is supposed to
          # have this value.

          # The code up to this line may have depicted the standard
          # output of a Ruby statement. If so, that's at an end now.
          create_assertion if == :stdout and @assertion.code

          expression, value = \
            loc.split(EXPRESSION_VALUE_COMMENT, 2).collect { |x| x.strip }
          @assertion.should_give = [value]
          @assertion.code << expression unless expression.empty?

This section handles all other lines of code:

          # This line of code is just another Ruby statement.

          # The code up to this line may have depicted the result or
          # standard output of a Ruby statement. If so, that's now at
          # an end.
          create_assertion unless @assertion.should_give.empty?

          @assertion.code << loc unless loc.empty?
    create_assertion # Finish up the last assertion

  # A convenience method to append the existing assertion (if any) to the
  # list, and create a new one.
  def create_assertion
    if @assertion && !@assertion.code.empty?
      @assertions << @assertion
    @assertion =[], should_give=[], how=:value)

Pages: 1, 2, 3, 4

Next Pagearrow

Sponsored by: