Ruby Behavior Driven Development Tutorial

Exercise 9: Packaging and Deployment

by Richard Kuehnel

This exercise covers the following topics.

  1. eliminating redundancy from Sinatra routes
  2. writing examples to create a "quick start" for users
  3. allowing users to try out your app without doing a full RubyGem install
  4. Rake clobber versus clean
  5. configuring Rake to generate TAR and ZIP files
  6. executing arbitrary shell scripts from Ruby code
  7. verifying your RubyGem's installation
  8. gem dependency and version management

Refactor!

Notice how little code there is in bin/sinatra. Compare this to a REST service implemented using Java's HttpServlet class, Spring MVC, or Apache CXF! As short as it is, however, some redundancy probably exists. Use your passing tests as a safety net to DRY up your code. Here are some hints:

  1.   content_types = {
        'html' => 'text/html',
        'xml' => 'text/xml',
        'json' => 'application/json'
      }
    
  2. This defines /statistics.html, /statistics.xml, and /statistics.json:
      get '/statistics.*' do
        ...
      end
    
  3. Here is how to get parameters from the HTTP request:
      get '/statistics.*' do
        scheme = request.scheme  # -> "http"
        host = request.host      # -> "localhost" for example
        port = request.port      # -> "8080" for example
        path = request.path      # -> "/statistics.html" for example
        ...
      end
    
  4. Two ways to accomplish the same thing:

      my_object.its_method()
      my_object.send("its_method")
    

Give Your Users Some Examples

Examples enable users to quickly grasp how your software works. In contrast to production code in the lib directory, example files in the examples directory should not correspond to class names. Instead, their names should describe what they do.

Unlike the executables in bin, the names of example files usually have a .rb extension.

Create a new directory called examples and insert the following into a new file examples/basic_api_usage.rb

#!/usr/bin/env ruby
# Basic extensions to Ruby's Array class.

$LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'

require 'statowl/extensions'

puts [1,2,3,4].mean                # -> 2.5
puts [1,2,3,4,5].median            # -> 3
puts [1,2,3,4].variance            # -> 1.25
puts [1,2,3,4].standard_deviation  # -> 1.118033988749895
puts
puts [1,2,3,4].statistics_as_text
puts
puts [1,2,3,4].statistics_as_html
puts
puts [1,2,3,4].statistics_as_xml
puts
puts [1,2,3,4].statistics_as_json

The $LOAD_PATH line is important, because users often like to run the examples without fully installing the project's RubyGem, as in

cd examples
ruby basic_api_usage.rb

Make the file executable and test it:

chmod +x examples/basic_api_usage.rb
examples/basic_api_usage.rb

Packaging a Gem

In preparation for packaging, remove the links to the test code status pages from the README.rdoc file. Only RDOC pages will deploy with the packaged gem.

There are several ways to package a gem. A very popular approach uses the gem command. Try the following from your project's working directory:

gem build statowl.gemspec

You should now have statowl-0.0.1.gem in your working directory. This works for any project that conforms to Ruby conventions and it is worthwhile to verify that users can package your gem this way.

Remove statowl-0.0.1.gem and take a look at the currently valid Rake tasks, some of which you created in your Rakefile and others that are set up by default.

rake --tasks

The clean task cleans out directories and removes any files that were generated as part of the build process but are not part of the final deliverable. The documentation mentions .o files, which are not created by a pure Ruby project. If you execute rake clean and examine your directories then you will notice that nothing really happens.

The clobber task is similar to clean but more aggressive. In particular, it removes the pkg directory that the package task creates. To include Aruba's temporary directory tmp, add the following to Rakefile:

CLOBBER.include 'tmp'

Also in your Rakefile, add steps to Gem::PackageTask to execute rake yard and to create archived versions prior to packaging the project as a RubyGem:

Gem::PackageTask.new(spec) do |pkg|
  pkg.need_tar = true
  pkg.need_tar_gz = true
  pkg.need_tar_bz2 = true
  pkg.need_zip = true
end

task :package => :yard

Execute the following to be sure that it removes any existing pkg directory and packages your gem as pkg/statowl-0.0.1.gem:

rake clobber
rake package

List the files in the pkg directory to check that the archived files were created. Which compression technique produced the smallest file?

Add a new Rake task called deploy that deploys your gem locally using the shell command

gem install pkg/statowl-0.0.1.gem

Hints

  1. Do not put "0.0.1" in your Rakefile. The version number should exist only in one place: lib/statowl/version.rb and it should be namespaced.
  2. There are many ways to execute a shell script from within Ruby. This, for example, executes the command ls -l seven times and echos the results to stdout:
    puts 'ls -l'            # use backticks, not single quotes!
    puts %x{ ls -l }
    puts %x[ ls -l ]
    puts %x( ls -l )
    puts %x< ls -l >
    system("ls -l")
    exec("ls -l")           # also terminates the current process
    puts "I'm dead!"        # never reached
    
  3. This an example of a simple task that gets the version number from lib/statowl/version.rb:
    require 'statowl'
    
    desc 'Give the Statowl version'
    task :version do
      puts Statowl::VERSION
    end
    

Make the deploy dependent on the package task:

...

task :deploy => :package

...

Execute the new task and ensure that Statowl gem was installed:

rake clobber
rake deploy
gem list | grep statowl

You should now be able to execute your command line app using the installed gem. This is how the users of your gem wil invoke your executable. You use bundle exec only to execute uninstalled code.

Try these commands:

statowl help
statowl help all
echo '1,2,3,4' | statowl all
echo '1,2,3,4' | statowl -f html all
echo '1,2,3,4' | statowl -f xml all
echo '1,2,3,4' | statowl -f json all

Now execute sinatra and point your browser to

http://localhost:8080/statistics.html?numbers=1,2,3,4

Version Control

Take a look at Gemfile.lock to see how dependencies were resolved. A dependency marked >=3.1.5 means any version greater or equal to 3.1.5. A dependency marked ~>3.1.5 is "pessimistic" and equivalent to >=3.1.5 AND <4.0.0. It is common practice to guarantee that API interfaces do not change in minor releases. Accordingly, an API change requires an increment of the major version number.

Note the simplecov version number. If you wanted to express some pessimism about future API changes in simplecov, you could modify your dependency in statowl.gemspec like this:

  s.add_development_dependency('simplecov', '~>0.9.1')

The "PLATFORM" in Gemfile.lock may be "ruby," which means C Ruby (MRI) on Rubinious but not Windows. Other possibilities include "jruby," "mri" (same as "ruby" but not Rubinious), "rbx" (same as "ruby" but not MRI), and various Windows configurations. The exclamation point after your project name indicates that the gem comes from your local working directory.

Cleanup

Completely uninstall the StatOwl gem so that it does not conflict with further development:

gem uninstall statowl

Exercise Summary

In this exercise you refactored your Sinatra routes by removing redundancy. You wrote "quick start" examples for users of your StatOwl API. You then configured your Rakefile to package and deploy your project as a RubyGem. Finally, you installed your gem.

Ruby can appear to be magic. This is facilitated by a focus on easy installation and use. Rubyists use convention over configuration so that README files are short. They include examples and they test and test and test to make sure the product works as expected. This is the Ruby way!

In the next exercise you will learn how Behavior Driven Development can be used for database applications.