Ruby Behavior Driven Development Tutorial

Exercise 6: Convention over Configuration

by Richard Kuehnel

This exercise covers the following topics.

  1. parsing XML
  2. using GLI to parse command line flags
  3. using Aruba's all_output method
  4. using Builder to write HTML
  5. using Builder for the 3 main styles of XML
  6. basic XPath to extract tag attributes and text nodes
  7. using Ruby's send method to support Convention over Configuration
  8. writing and parsing JSON

Cycle Nine - Formatting for HTML

It is time to fulfill another promise specified by your project home page: HTML formatting. What is your first step in fulfilling this objective?
Write a failing test.
  

When testing structured output like XML and JSON, it is best to find a parsing tool rather than compare lengthy strings. This ensures that newlines, whitespace, comments, and other insignificant text do not cause tests to fail unnecessarily.

Insert the following feature into a new file features/html.feature:

Feature: Format statistics for HTML
  If I compute statistics
  I want to easily convert them to HTML

  Scenario: Get HTML
    Given a file named "infile" with:
      """
      1,2,3,4
      """
    When I run `bundle exec statowl -f html all` interactively
    And I pipe in the file "infile"
    Then the output is XML
    And XPath "/html/head/title" includes "Statistics"
    And XPath "/html/body/p" includes "Mean: 2.5"
    And XPath "/html/body/p" includes "Median: 2.5"
    And XPath "/html/body/p" includes "Variance: 1.25"
    And XPath "/html/body/p" includes "Standard Deviation: 1.118033988749895"

This feature adds a new options flag -f html to the command line app. The executable file bin/statowl currently ignores this flag, so the all command's output will still be plain text.

Execute rake features and copy the code snippets for the undefined steps to a new file features/step_definitions/command_steps.rb.

Your steps should now be defined, but pending or skipped. Implement the first step in features/step_definitions/command_steps.rb by parsing the results from Aruba's all_output method:

require 'rexml/document'

include REXML

Then(/^the output is XML$/) do
  @document = Document.new(all_output)
end
...

This step passes, even though all_output returns text that is not valid XML. Implement the second step definition by getting the text contained in the XPath argument:

Then(/^XPath "(.*?)" includes "(.*?)"$/) do |x_path, expected_text|
  found = false
  @document.elements.each(x_path) do |element|
    if element.text == expected_text
      found = true
    end
  end
  expect(found).to eq(true)
end

This step fails. Modify bin/statowl to read the -f html flag and return valid XHTML:

...
desc 'Describe some flag here'
default_value 'the default'
arg_name 'The name of the argument'
flag [:f,:flagname]
desc 'Format the results'
default_value 'text'
arg_name 'format'
flag [:f,:format], :desc => 'text or html'

...
desc 'Computes all statistics'
command :all do |c|
  c.action do |global_options,options,args|
    if global_options[:f] == "html"
      puts """
           <!DOCTYPE html>
           <html>
             <head>
               <title>Statistics</title>
             </head>
             <body>
               <p>Mean: 2.5</p>
               <p>Median: 2.5</p>
               <p>Variance: 1.25</p>
               <p>Standard Deviation: 1.118033988749895</p>
             </body>
           </html>
           """
    else
      puts array_from_STDIN.statistics_as_text
    end
  end
end
...

Your Cucumber scenario should now pass. Check out the new formatting options:

bundle exec bin/statowl help

Then try them for yourself:

echo "1,2,3" | bundle exec bin/statowl -f html all
echo "1,2,3" | bundle exec bin/statowl --format=html all

Obviously this trivial solution will not meet project requirements, but your tests pass, so it is time to think about refactoring.

Refactor!

There is a lot of refactoring to do before implementing a useful solution. First, you are once again violating the Single Responsibility Principle, because the executable bin/statowl fails to delegate business logic. Write a new statistics_as_html method for the extensions of the Array class in lib/statowl/extensions.rb:

...
  def statistics_as_html
    """
    <!DOCTYPE html>
    <html>
      <head>
        <title>Statistics</title>
      </head>
      <body>
        <p>Mean: 2.5</p>
        <p>Median: 2.5</p>
        <p>Variance: 1.25</p>
        <p>Standard Deviation: 1.118033988749895</p>
      </body>
    </html>
    """
  end
...

Now you can delegate HTML formatting and eliminate it from bin/statowl:

...
desc 'Computes all statistics'
command :all do |c|
  c.action do |global_options,options,args|
    if global_options[:f] == "html"
      puts array_from_STDIN.statistics_as_html
    else
      puts array_from_STDIN.statistics_as_text
    end
  end
end
...

Still green? Just as it makes sense to use a parser to read structured text like XML and JSON, it is reasonable to use a builder to write it. Ruby builders are powerful, but they can be confusing. Fortunately, you have automated tests to check that your builder is performing as expected.

Replace your hard-coded string in lib/statowl/extensions.rb with a hard-coded, but much more elegant, builder:

require 'builder'
...
  def statistics_as_html
    title = "Statistics"
    @builder ||= Builder::XmlMarkup.new(:indent => 2)
    @builder.declare! :DOCTYPE, :html
    @builder.html do
      @builder.head{ @builder.title title }
      @builder.body do
        @builder.h1 title
        @builder.p "Mean: 2.5"
        @builder.p "Median: 2.5"
        @builder.p "Variance: 1.25"
        @builder.p "Standard Deviation: 1.118033988749895"
      end
    end
  end
...

Insert a dependency on the Builder gem in statowl.gemspec:

  s.add_runtime_dependency('builder')

Still green? Now replace the hard-coded <p> tags with real numbers:

...
  def statistics_as_html
    title = "Statistics"
    @builder ||= Builder::XmlMarkup.new(:indent => 2)
    @builder.declare! :DOCTYPE, :html
    @builder.html do
      @builder.head{ @builder.title title }
      @builder.body do
        @builder.h1 title
        statistics.each do |key, value|
          @builder.p "#{key.to_s.split("_").map{ |word| word.capitalize}.join(" ")}: #{value}"
        end
      end
    end
  end
...

Do all scenarios pass? Compare the text and HTML formats:

echo '1,2,3,4' | bundle exec bin/statowl all
echo '1,2,3,4' | bundle exec bin/statowl -f html all

More Refactoring

Take a look at your for the statistics_as_text and statistics_as_html methods in lib/statowl/extensions.rb. What do these lines do?

...
    statistics.map do |key, value|
      "#{key.to_s.split('_').map{ |word| word.capitalize }.join(' ')}: #{value}\n"
    end
...
        statistics.each do |key, value|
          @builder.p "#{key.to_s.split("_").map{ |word| word.capitalize}.join(" ")}: #{value}"
        end
...

You wrote these recently and yet your comprehension of their purpose is probably already a little fuzzy. Remember being tempted to use Extract Method refactoring on this? It was a good idea then and it is even more appropriate now. These lines do not read like you would describe them. Put the logic into a separate method and make it private, which tells the reader of your code that its implementation is not important:

...
  def statistics_as_text
    statistics.map do |key, value|
      "#{replace_underscores_and_capitalize(key)}: #{value}\n"
    end
  end

  def statistics_as_html
    title = "Statistics"
    @builder ||= Builder::XmlMarkup.new(:indent => 2)
    @builder.declare! :DOCTYPE, :html
    @builder.html do
      @builder.head{ @builder.title title }
      @builder.body do
        @builder.h1 title
        statistics.each do |key, value|
          @builder.p "#{replace_underscores_and_capitalize(key)}: #{value}"
        end
      end
    end
  end

  private

  def replace_underscores_and_capitalize(key)
    key.to_s.split("_").map{ |word| word.capitalize}.join(" ")
  end
...

Still Green?

Formatting for XML

There are several styles of XML, all of which can be implemented using the Builder gem. Tags can be used as keys for key-value pairs, for example:

<statowl>
  <array>1.0,2.0,3.0,4.0,5.0</array>
  <mean>3.0</mean>
  <median>3.0</median>
  <variance>2.0</variance>
  <standard_deviation>1.4142135623730951</standard_deviation>
</statowl>

This is implemented in Builder by passing the attribute name and its value to the tag! method:

      @builder.array map{ |number| number.to_s }.join(',')
      statistics.each do |key, value|
        @builder.tag!(key, value)
      end

Fixed tags have a more standardized appearance but are verbose and require a lot of typing:

<statowl>
  <array>
    <value>1.0,2.0,3.0,4.0,5.0</value>
  </array>
  <statistics>
    <statistic>
      <name>mean</name>
      <value>3.0</value>
    </statistic>
    <statistic>
      <name>median</name>
      <value>3.0</value>
    </statistic>
    <statistic>
      <name>variance</name>
      <value>2.0</value>
    </statistic>
    <statistic>
      <name>standard_deviation</name>
      <value>1.4142135623730951</value>
    </statistic>
  </statistics>
</statowl>

The style is implemented by passing the tag contents to a method corresponding to the tag name:

      @builder.array do
        @builder.value map{ |number| number.to_s }.join(',')
      end
      @builder.statistics do
        statistics.each do |key, value|
          @builder.statistics do
            @builder.name key.to_s
            @builder.value value.to_s
          end
        end
      end

Putting data into attributes is less wordy but more difficult to read:

<statowl>
  <data array="1.0,2.0,3.0,4.0,5.0"/>
  <statistics>
    <statistic mean="3.0"/>
    <statistic median="3.0"/>
    <statistic variance="2.0"/>
    <statistic standard_deviation="1.4142135623730951"/>
  </statistics>
</statowl>

This is implemented by passing a hash to a non-existing method whose name is the tag name:

      @builder.data 'array' => map{ |number| number.to_s }.join(',')
      @builder.statistics do
        statistics.each do |key, value|
          @builder.statistic key => value
        end
      end

Whichever style you prefer, Builder makes it easy to format the XML and to insert processing instructions:

  def statistics_as_xml
    @builder ||= Builder::XmlMarkup.new(:indent => 2)
    @builder.instruct!
    @builder.statowl do
      # Your XML style goes here
    end
  end

The instruct! method writes

<?xml version="1.0" encoding="UTF-8"?>

Write a new feature called features/xml.feature that tests for the XML style that you prefer. The production code should be able to handle this:

echo '1,2,3,4' | bundle exec bin/statowl -f xml all

You will need to write the new Cucumber feature, add step definitions as necessary, handle the -f xml and --format=xml flags in your GLI-based executable, and write an statistics_as_xml method for your Ruby extensions to the Array class.

Tip: For the XML
<statowl>
  <statistics>
    <statistic mean="3.0"/>
  </statistics>
</statowl>
this will set found equal to true:
  found = false
  @document.elements.each("statowl/statistics/statistic") do |element|
    if element.attributes["mean"] == "3.0"
      found = true
    end
  end

Convention over Configuration

Consider what it would take to create a JSON view:

  1. add a new method statistics_as_json to lib/statowl/extensions.rb
  2. add new flag handling code to bin/statowl
Configuring two production files to add a single feature is not the Ruby way! Well-grounded Rubyists pride themselves on convention over configuration. Step 1 is reasonable. Step 2, on the other hand, reeks of duplication, violates the Single Responsibility Principle, and creates unnecessary extra work. The -f html flag causes a call to the statistics_as_html method. The -f xml flag causes a call to the statistics_as_xml method. What do you think the -f json flag should call?
It is not statistics_as_fortran!
  

For comparison, consider Java's HttpServlet class. When you extend the class to create something like WeatherServlet then you still need to configure the web.xml file to point the URL /weather to the new servlet. Configuration is required because you might want to point a different URL to the servlet, perhaps something like /dogbowl. Or maybe you want WeatherServlet to handle /weather/temperatures.xml but want DogbowlServlet to handle weather/temperatures.json. The class requires both Java code and XML configuration to make anything work. There is no default behavior. Argh!

Your implementation of the all command in bin/statowl probably looks similar to this:

...
desc 'Computes all statistics'
command :all do |c|
  c.action do |global_options,options,args|
    if global_options[:f] == "html"
      puts array_from_STDIN.statistics_as_html
    elsif global_options[:f] == "xml"
      puts array_from_STDIN.statistics_as_xml
    else
      puts array_from_STDIN.statistics_as_text
    end
  end
end
...

Eliminate the duplication by parsing the flag and assembling the method name:

...
desc 'Computes all statistics'
command :all do |c|
  c.action do |global_options,options,args|
    method_name = "statistics_as_#{global_options[:f] || 'text'}"
    puts array_from_STDIN.send(method_name)
  end
end
...

Are your Cucumber scenarios still green? Now you are ready to add an statistics_as_json method to lib/statowl/extensions.rb. The default command line behavior is to call this method in response to a -f json or --format=json flag. The executable file bin/statowl does not need to be configured to add an additional format as long as you adhere to the convention that you have established.

Formatting for JSON

Add JSON output to your app. Make it include the original array like this:

      {
        "array": [1.0,2.0,3.0,4.0],
        "mean": 2.5,
        "median": 2.5,
        "variance": 1.25,
        "standard_deviation": 1.118033988749895
      }

Hints

  1. Start by writing a new Cucumber scenario
  2. In Ruby, a Hash is easily converted to and from a JSON string. Moreover, the Hash#eql? method performs a deep comparison, which is handy for a simple test. The methods Hash#== or Hash#eql only make shallow checks to ensure that each hash has the same number of keys and that each key-value pair is equal according to ==. Here is an example of a deep comparison:
    require 'json'
    
    my_json = the_original_hash.to_json
    the_same_hash = JSON.parse(my_json)
    expect(the_same_hash.eql?(the_origial_hash)).to eql(true)
    
  3. Add a dependency for the 'json' gem to statowl.gemspec.
  4. To merge a hash of the computed statistics into a hash that contains the original array, you can use the merge method of Ruby's Hash class:
      def as_hash
        { :array => self }.merge(statistics)
      end
    

Exercise Summary

In this exercise you parsed XML and searched its content using XPath. You configured GLI to parse command line flags. You also gained a lot of experience with Builder and saw how it creates three different styles of XML. You implemented convention over configuration using Ruby's send method. Finally, you wrote and parsed JSON with amazingly little code. (How many lines of production code did it take you to implement the statistics_as_json method? Can you do it in less?)

In the next exercise you will learn about code quality metrics.