Ruby Behavior Driven Development Tutorial

Exercise 5: Test >> Code >> Refactor

by Richard Kuehnel

This exercise covers the following topics.

  1. delegating business logic to keep executables short
  2. eliminating redundancy in Cucumber scenarios
  3. using the facets gem to eliminate Ruby indent spasms
  4. implementing the Single Responsibility Principle

Refactor!

In four different places the executable bin/statowl converts arrays of strings into arrays of floating-point numbers. That's quadruplication! With green tests protecting you from mistakes, extract the logic in bin/statowl to a separate method:

...
desc 'Computes the mean'
command :mean do |c|
  c.action do |global_options,options,args|
    puts array_from_STDIN.mean
  end
end

desc 'Computes the median'
command :median do |c|
  c.action do |global_options,options,args|
    puts array_from_STDIN.median
  end
end

desc 'Computes the variance'
command :variance do |c|
  c.action do |global_options,options,args|
    puts array_from_STDIN.variance
  end
end

desc 'Computes the standard deviation'
command :deviation do |c|
  c.action do |global_options,options,args|
    puts array_from_STDIN.standard_deviation
  end
end
...

def array_from_STDIN
  STDIN.read.strip.split(',').map{ |number| number.to_f }
end

Still green? Delegate the conversion of strings to floats to a single location in the business logic by adding the mapping to lib/statowl/extensions.rb:

...
  def as_f
    map{ |number_as_string| number_as_string.to_f }
  end
...

Then in bin/statowl shorten

  STDIN.read.strip.split(',').map{ |number| number.to_f }

to

  STDIN.read.strip.split(',').as_f

Still green? Take a look at features/text.feature. Remove the redundant setups by creating a Background for the scenarios:

Feature: Get statistics from numbers via the command line
  If I have some numbers
  I want to easily compute statistics on them

  Background:
    Given a file named "infile" with:
      """
      1,2,3,4,5
      """

    Scenario: Get Help
      When I run `bundle exec statowl help`
      Then the output should contain:
        """
        statowl [global options] command
        """

    Scenario: Get the mean
      When I run `bundle exec statowl mean` interactively
      And I pipe in the file "infile"
      Then the output should contain:
        """
        3.0
        """

    Scenario: Get the median
      When I run `bundle exec statowl median` interactively
      And I pipe in the file "infile"
      Then the output should contain:
        """
        3.0
        """

    Scenario: Get the variance
      When I run `bundle exec statowl variance` interactively
      And I pipe in the file "infile"
      Then the output should contain:
        """
        2.0
        """

    Scenario: Get the standard deviation
      When I run `bundle exec statowl deviation` interactively
      And I pipe in the file "infile"
      Then the output should contain:
        """
        1.414
        """

Still green? Automated tests make refactoring production code and test code safe and easy.

Cycle Eight

StatOwl's project description states that the output format can be plain text, HTML, XML, or JSON. Add another scenario to features/text.feature that tests your command line app's ability to format "all" statistics for plain text:

...
    Scenario: Get nicely formatted statistics
      When I run `bundle exec statowl all` interactively
      And I pipe in the file "infile"
      Then the output should contain:
      """
      Mean: 3.0
      Median: 3.0
      Variance: 2.0
      Standard Deviation: 1.414
      """

The scenario should fail. Add production code to bin/statowl to make it pass:

...
desc 'Computes all statistics'
command :all do |c|
  c.action do |global_options,options,args|
    array = array_from_STDIN
    puts %{Mean: #{array.mean}
Median: #{array.median}
Variance: #{array.variance}
Standard Deviation: #{array.standard_deviation}}
  end
end
...

Try out the new feature from the command line:

echo "1,2,3,4,5" | bundle exec bin/statowl all

This works OK, but look at those terrible indent spasms in bin/statowl to make the output flush with the left margin. This occurs frequently in multi-line strings and makes code difficult to read. The Facets gem adds a convenient unindent method to correct this problem. Add a Facets dependency to statowl.gemspec:

...
  s.add_runtime_dependency('facets')
...

Now you can clean up your margins in bin/statowl:

require 'facets'
...
desc 'Computes all statistics'
command :all do |c|
  c.action do |global_options,options,args|
    array = array_from_STDIN
    puts """
         Mean: #{array.mean}
         Median: #{array.median}
         Variance: #{array.variance}
         Standard Deviation: #{array.standard_deviation}
         """.unindent.lstrip
  end
end
...

Verify that all of your tests are passing. Then take a look at the unindented left margin for yourself:

echo "1,2,3,4,5" | bundle exec bin/statowl all
According to the Single Responsibility Principle, every class should have a single responsibility that should be entirely encapsulated by the class. All its services should focus on that responsibility. This applies to any file that contains production code. Where is your code most flagrantly violating the SRP?
bin/statowl
  

This file will look really ugly when you add HTML, XML, and JSON capability. Some major refactoring is needed before you implement the next feature. Using your automated tests to keep bugs from arising, pull out knowledge of the various types of statistics and put it where it belongs by adding the following to lib/statowl/extensions.rb:

...
  def statistics
    keys = [ :mean, :median, :variance, :standard_deviation ]
    values = keys.map{ |key| self.send(key) }
    Hash[ keys.zip(values) ]
  end
...

Now you can get the statistics as a generic hash. With the following modification bin/statowl has no knowledge of what the hash actually contains:

...
desc 'Computes all statistics'
command :all do |c|
  c.action do |global_options,options,args|
    array_from_STDIN.statistics.each do |key, value|
      label = key.to_s.split("_").map{ |word| word.capitalize }.join(" ")
      puts "#{label}: #{value}"
    end
  end
end
...

Still green? This represents an improvement, because bin/statowl no longer needs to know about specific statistics. It merely formats a generic hash. Nevertheless, it still does too much. Move the formatting to lib/statowl/extensions.rb:

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

Now your command line executable can delegate all formatting:

...
desc 'Computes all statistics'
command :all do |c|
  c.action do |global_options,options,args|
    puts array_from_STDIN.statistics_as_text
  end
end
...

More refactoring? Take a look at this line in bold:

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

This is a prime candidate for Extract Method refactoring. When the purpose of a line of code is to replace underscores with spaces and capitalize the results, then the code should say that. Extract Method refactoring is great for code that is too long or needs comments. Control your refactoring urges for now, however. You will extract the method in the next exercise.

You have implemented the Single Responsibility Principle. The executable bin/statowl has no knowledge of how to compute a complete set of mathematical statistics, nor does in know how to format them as plain text. lib/statowl/extensions.rb does not know the source of its numbers nor what is done with the results. The files now have completely separate concerns.

Automated tests facilitate aggressive refactoring. They create a safety net to keep delivery risks low and Ruby elegance high.

Exercise Summary

In this exercise you delegated business logic to keep your executable file short. You learned how to eliminate redundancy using Cucumber background statements. Finally, you refactored your code to conform with the Single Responsibility Principle. Along the way you gained additional practice with the Test-Code-Refactor mantra of Test Driven Development.

In the next exercise you will learn how to implement Ruby "Convention over Configuration" for a clean design.