Ruby Behavior Driven Development Tutorial

Exercise 4: Testing Command Line Apps

by Richard Kuehnel

This exercise covers the following topics.

  1. using Aruba to test command line applications
  2. identifying the syntax of Cucumber's currently defined step definitions
  3. modifying Aruba step definitions

Cycle Three

Execute the following:

cucumber --format stepdefs

These are the step definitions that are currently defined, first the ones that you wrote and then the ones that are included with the RubyGem Aruba, which is included as a dependency in your statowl.gemspec file. Aruba bundles Cucumber step definitions for testing command line applications. The number preceding each definition is the average time, in seconds, that it takes to execute the step, if known.

Create a new feature file features/text.feature and insert the following into it:

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

  Scenario: Get Help
    # This line contains two single backquotes:
    When I run `bundle exec statowl help`
    Then the output should contain:
      """
      statowl [global options] command
      """

Execute rake features. The scenario passes because GLI already includes a help message that includes a usage statement. Steps like When I run... and Then the output should... are two of the many steps that are defined by Aruba. Execute again

cucumber --format stepdefs

Notice that it takes a long time to execute (When) I run.... This is because a new shell needs to be created to execute the command.

The majority of steps defined by Aruba pertain to command line applications, but others are more general.

Tip: The key to using Aruba is to look through its step definitions and copy the exact wording. If your scenario has When I execute... instead of When I run..., for example, then Cucumber will complain that the step is undefined.

The source code for Aruba is included in its RubyGem. If there is an Aruba step that almost meets your needs, then you can copy its step definition to your own step definition file and modify it there. cucumber --format stepdefs gives you the path to the file and the line number for the step definition. To find out where your RubyGems are installed, so that you can get to the Aruba source code, execute gem env.

You have one simple test and you wrote no production code to make it pass, so there is not much refactoring to do.

Cycle Four

The next feature you need to implement gives users the ability to this:

echo "1,2,3,4,5" > infile
statowl mean < infile

or to and from a file:

echo "1,2,3,4,5" > infile
statowl mean < infile > outfile

or perhaps from the command line like this:

echo "1,2,3,4,5" | statowl mean

Add another Scenario to features/text.features to get the mean of comma-separated numbers entered on the command line:

...

  Scenario: Get the mean
    Given a file named "infile" with:
      """
      1,2,3,4,5
      """
    When I run `bundle exec statowl mean` interactively
    And I pipe in the file "infile"
    Then the output should contain:
      """
      3.0
      """

Execute rake features to see that the steps are already defined by Aruba. The scenario fails when you run it because you haven't implemented the mean command for your command line app. Implement it by modifying bin/statowl:

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

All green? The hard-coded number is not there for triangulation - you already know that your API accurately computes the mean. The reason for starting with hard-coded output is to test the plumbing of your command line app. It is now clear that you scenario calls the correct action.

Try this from the command prompt:

echo "4,5,6" | bundle exec bin/statowl mean

Wrong answer. Remove the hard-coded value 3.0 and replace it with some production code to actually compute the mean by using your convenient statistics API:

...
require 'statowl'
require_relative '../lib/statowl/extensions'
...
desc 'Computes the mean'
command :mean do |c|
  c.action do |global_options,options,args|
    array = STDIN.read.strip.split(',').map{ |number| number.to_f }
    puts array.mean
  end
end
...
You wrote a failing test. Then you wrote production code to make it pass. What is the next step?
Refactor!
  

There are issues with these two lines:

    array = STDIN.read.strip.split(',').map{ |number| number.to_f }
    puts array.mean

They violate the Single Responsibility Principle. bin/statowl knows how to parse command line arguments. It also knows that STDIN will contain a string of comma-separated numbers that need to be turned into an array of floating point numbers. Moreover, it knows how to compute statistics. It has too many responsibilities!

Generally the executable should only parse command line arguments and echo back any output to the console. For clarity and testability it should delegate all business logic. Nevertheless, we are only talking about two lines of code, so leave them alone for now. In the next exercise you will delegate some of these details.

Cycles Five, Six, and Seven

Implement the median, variance, and deviation commands for bin/statowl by writing more acceptance tests using Cucumber and Aruba. Any feelings you get that the tests and production code have redundancies are completely justified. You will mitigate this problem and make your code more Rubyesque in the next exercise.

Exercise Summary

In this exercise you used the Aruba gem to test your command line application. While it is relatively easy to copy a Cucumber code snippet to a step definition file and implement it yourself, it is also worthwhile to become aware of the many gems available to do the work for you.

In the next exercise you will get much more practice in the test-code-refactor mantra of Behavior Driven Development.