The Ruby Ecosystem for New Rubyists

Share this article

newrubyist

Ruby is more than just a language. It has a universe of tools and processes supporting the creation of the complex software it makes. This can be overwhelming to newcomers, so I’ve put together an article that will hopefully make things a little more clear.

Version Management

Let’s say you have two projects relying on two different versions of a gem. The project that uses the newer version is not compatible with the older gem. What do you do?

One option is to just install whichever gem you need at the time. This isn’t a great idea, because one or both of the gem’s versions might have dependency version mistakes/impreciseness in its Gemfile or gemspec. That kind of problem is hard to track down.

Instead, most Rubyists rely on some kind of version manager. Not only do version managers keep gems tidy, but they separate Ruby implementations as well. This makes it easy to test for differences between, say, Ruby 2.0 and Ruby 2.1.

Popular version management solutions include RVM, rbenv, and Uru(Windows).

Here is how you would get started managing Rubies with RVM:

Install RVM.

$ \curl -sSL https://get.rvm.io | bash

Source RVM script in bash-compatible shell on its startup.

$ echo "source $HOME/.rvm/scripts/rvm" >> ~/.bash_profile

Reload the shell.

$ source ~/.bash_profile

Verify that RVM script has been sourced. It should print “rvm is a function.”

$ type rvm | head -n 1

Install Ruby 2.0.0.

$ rvm install 2.0.0

Switch to 2.0.0.

$ rvm use 2.0.0

Create a new gemset for 2.0.0 named “experimental.”

$ rvm gemset create experimental

Switch to the new gemset.

$ rvm gemset use experimental

Crafting Gems

Packaged code was not always a thing in Ruby. In 2003, rubyforge.org launched as a place for Ruby developers to share code. Although it improved the situation a bit, developers were still on their own when it came to figuring out how to run each other’s code. In November 2003, some Ruby developers got together and decided to solve the problem forever. In 2004 rubygems.org launched, and with it, the gem tool.

RubyGems is a package manager for Ruby libraries and programs. Here are a couple of reasons one might have for creating a Ruby gem:

  1. Easily share code with other developers
  2. Avoid duplicating code between projects

Installing a gem is easy:

$ gem install gem_name

The layout for a typical gem project looks like this:

- spec
  - gem_name_spec.rb
  - spec_helper.rb
- bin
  - gem_name
- lib
  - gem_name.rb
  - gem_name
    - source_file1.rb
    - source_file2.rb
    - source_file...
    - version.rb
- Gemfile
- gem_name.gemspec
- README.md
- LICENSE
- Rakefile

It isn’t necessary to memorize the gem directory structure. A simple way to generate a basic gem template is with the bundler tool.

$ bundle gem gem_name

Note: Bundler likely comes pre-installed with your Ruby installation, but if not or if you want the latest version you can install the gem:

$ gem install bundler

If you don’t like the template that Bundler creates, there are other tools available, including jeweler and hoe.

The Load Path

We need to examine a non-obvious issue that new gem developers face: Ruby’s load path. Let’s say you have a couple of files in the same directory (not making a gem, just in general). We’ll call them source1.rb and source2.rb.

# source1.rb
require 'source2'

# source2.rb
puts "hello world"

Looks great, but try using the file and see what happens.

$ ruby source1.rb
...'require': cannot load such file -- source2 (LoadError)...

Wait, what is going on? Even though they are in the same directory, source1.rb can’t see source2.rb. It turns out that Ruby does not automatically include the directory of a source file it executes in its load path. We can make this example work by telling it to do so with the command line option -I (include directory) and . (which directory – . is the current directory):

$ ruby -I . source1.rb
hello world

Alternatively, you can require the direct path to the file:

# source1.rb 
require './source2'

So do you need to do something like this when developing gems? No. For testing gems in development, source directories can be programmatically added to Ruby’s $LOAD_PATH global variable.

# source1.rb
$LOAD_PATH.unshift(".")
require "source2"

This is used to include the lib folder and all of its subdirectories. You will typically encounter File::expand_path converting the relative path to the lib folder from the file to the absolute path.

# spec/spec_helper.rb
lib = File.expand_path('../../lib', __FILE__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)

# now we can require lib/gem_name.rb which can require other code as needed
require 'gem_name'

The $LOAD_PATH variable is just an array. $LOAD_PATH.unshift(lib) adds the directory to the beginning of the array, so that it is loaded before anything else. Note that File.expand_path('../../lib', __FILE__) refers to the lib directory one directory up and not two as it looks like. This is a common trip-up. __FILE__ as the second argument specifies the directory to start in, otherwise the current working directory (the one Ruby was executed in) is used.

Note that you will not always see $LOAD_PATH in gems. A popular, if incredibly undescriptive, alias is $:.

$:.unshift(File.expand_path('../../lib', __FILE__)

Any supporting gem code should go in a folder of the same name as the gem.

- lib
  - gem_name.rb
  - gem_name
    - some_file.rb

# lib/gem_name.rb
require 'gem_name/some_file'

When a gem is installed, the contents of the lib directory are placed in a directory that is already in Ruby’s load path. Therefore, the load path shouldn’t be modified anywhere within the actual gem code, and the name of the gem should be unique.

gemspec

The rubygems gemspec file is the place where the gem is actually defined. The gemspec is the one file that must exist in order to build a gem.

# gem_name.gemspec
lib = File.expand_path("../lib", __FILE__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require 'gem_name/version'

Gem::Specification.new do |s|    
  s.platform       = Gem::Platform::RUBY
  s.name           = 'gem_name'
  s.version        = GemName::VERSION
  s.authors        = ['Your Name']
  s.email          = ['your@email.com']
  s.homepage       = 'https://github.com/your_name/gem_name'
  s.summary        = 'Gem in a few words'
  s.description    = 'Longer decription of gem'
  s.required_ruby_version = '>= 1.9.3'
  s.require_path   = 'lib'
  s.files          = Dir[LICENSE, README.md, 'lib/**/*']
  s.executables    = ['gem_name']

  s.add_dependency('sqlite3')
  s.add_development_dependency("rspec", ["~> 2.0"])
  s.add_development_dependency("simplecov")
end

Not all of these fields are required. For example, not every gem has executables and some might prefer to place their dependencies in a Gemfile instead. Be sure to check out the rubygems.org specification reference.

Gemfile

The Bundler Gemfile contains a list of the gem’s dependencies, their versions, and where to get them. This makes it easy for other developers to sync their environment with yours to ensure the same behavior. It also makes it possible for users of the gem to have all of its dependencies installed without having to know what they are.

A gem dependency in a Gemfile looks like this:

gem <gem_name>, <version constraint>

Here is an example of a Gemfile:

# Gemfile
source 'https://rubygems.org'

gem 'nokogiri', '~> 1.4.2'

group :development do
  gem 'sqlite3'  
end

group :test do
  gem 'rspec'
  gem 'cucumber'
end

group :production do
  gem 'pg'
end

gemspec

A non-obvious piece of syntax in Gemfiles is the so-called spermy operator (~>). This operator will increase the last digit until it rolls over. So gem 'nokogiri', '~> 1.4.2' is semantically equivalent to gem 'nokogiri", '>=1.4.2', '<1.5.0'.

The gemspec line just tells Bundler to install the dependencies listed in the gemspec file. Many prefer to place all of their dependencies in a Gemfile, so this line is not required.

When you want to sync your environment with a gem’s dependencies, just navigate to the directory containing the Gemfile and run:

$ bundle install

Sometimes projects have dependencies that only make sense for particular environments. If you don’t plan on using the gem in production, you can avoid installing the gems listed in the production group.

$ bundle install --without production

When Bundler calculates dependencies, it creates a file called Gemfile.lock. The purpose of this file is to help avoid subtle differences between development environments. A common question is whether this should go in a repository. If the project is a gem, no, but if it’s an app, yes – the reason being that an apps generally have their own separate gemsets, and it’s important that everyone developing an app has the same exact version of each dependency.

For more information, see the Bundler Gemfile reference and Bundler Rationale.

Rakefile

rake is the Ruby world’s equivalent to GNU make. Unlike Gemfiles and gemspecs, Rakefiles exist mostly for convenience. Having simple commands like rake test makes it easy for automated systems (or other developers) to test code without needing to know what kind of testing system to use.

# Rakefile
lib = File.expand_path("../lib", __FILE__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require "gem_name/version"

task :test do
  system "rspec"
end

task :build do
  system "gem build gem_name.gemspec"
end

task :release => :build do
  system "gem push gem_name-#{GemName::VERSION}"
end

If you want to get a good feel for how Rakefiles work, there is a long tutorial here.

Building Gems

Once a gem is properly organized and tested, it can be built. The gem build command will produce a .gem file that can be installed locally or pushed to a gem repository. The filename will include the version set in the gemspec.

$ gem build gem_name.gemspec
$ gem install gem_name-version.gem
$ gem push gem_name-version.gem

Now, in projects you or anybody who installs the gem can use it with:

require 'gem_name'

Note: You will sometimes see require 'rubygems' in code. This is a holdover from when RubyGems was a separate project. Starting with Ruby 1.9, RubyGems is part of the standard library.

Bundle Exec

You will often commands written like bundle exec rake db:migrate instead of rake db:migrate. This is because bundle exec guarantees that the command will be executed using the versions of the gems specified in the Gemfile. RVM creates gemsets with rubygems-bundler to avoid this issue, and it also shouldn’t be a problem if you are using Ruby 2.2, but it is worth knowing about.

Ruby Implementations

There are many implementations for the Ruby languages as well as Ruby-like languages. The most popular of these include:

  • MRI/CRuby – The reference implementation of Ruby written in C
  • JRuby – Ruby running on the Java Virtual Machine
  • Rubinius – Ruby written in Ruby, running on LLVM
  • RubyMotion – Commercial implementation for making native Apple / Android apps

There are many others.

There seems to be a common misconception that threads don’t work in Ruby. Threads are limited in CRuby (MRI), but they do provide the ability to work on two problems at once. It’s important to understand the difference between concurrency and parallelism:

  • concurrency – completing two or more tasks in overlapping time periods (i.e. downloading a file while waiting to see if the user interacts with the interface).
  • parallelism – completing two or more tasks simultaneously on multiple processor cores

The reference implementation of Ruby, MRI, has a global interpreter lock, so it can only use one native thread at a time. What this means is that Ruby threads can run concurrently but not in parallel. If executing code on multiple processors is needed in MRI, forking or some other variant of process coordination must be used (at least until the GIL is removed, if ever). There are Ruby implementations that lack a GIL, including all of the others listed above. On top of this, forking isn’t technically available to the JVM, so in JRuby you pretty much have to use threads, anyway.

I wrote an article into more detail about forking in Ruby here.

Documentation

When a gem is installed, you will often notice a notification about rdoc and ri documentation being installed. RDoc is an embedded documentation generator for Ruby. ri is a man-like tool for reading offline documentation in the terminal.

$ ri Array
$ ri Array#length
$ ri Math::sqrt

When using a version manager, it might be necessary to generate the core documentation manually. Otherwise, ri will just print “Nothing known about [whatever]”. For example, with rvm the core documentation can be generated with:

$ rvm docs generate

This can take a while. Likewise, when installing a gem, generating the documentation can take an inconvenient amount of time if the gem is complex enough. You can skip the documentation for the gem with:

$ gem install gem_name --no-rdoc --no-ri

Pry

Another way to explore the Ruby documentation is to install pry. Pry is an alternative to the irb console with various enhancements, including the ability to see the original C source code for methods.

$ gem install pry pry-doc
$ pry

[1] pry(main)> show-method Array#map

From: array.c (C Method):
Owner: Array
Visibility: public
Number of lines: 13

static VALUE
rb_ary_collect(VALUE ary)
{
    long i;
    VALUE collect;

    RETURN_SIZED_ENUMERATOR(ary, 0, 0, ary_enum_length);
    collect = rb_ary_new2(RARRAY_LEN(ary));
    for (i = 0; i < RARRAY_LEN(ary); i++) {
  rb_ary_push(collect, rb_yield(RARRAY_AREF(ary, i)));
    }
    return collect;
}

1] pry(main)> cd Array
[2] pry(Array):1> show-doc map

From: array.c (C Method):
Owner: Array
Visibility: public
Signature: map()
Number of lines: 12

Invokes the given block once for each element of self.

Creates a new array containing the values returned by the block.

See also Enumerable#collect.

If no block is given, an Enumerator is returned instead.

  a = [ "a", "b", "c", "d" ]
  a.collect { |x| x + "!" }        #=> ["a!", "b!", "c!", "d!"]
  a.map.with_index{ |x, i| x * i } #=> ["", "b", "cc", "ddd"]
  a                                #=> ["a", "b", "c", "d"]

Testing

Let’s say you clone some random repository, run some code metrics, and fix some duplication. You aren’t familiar with the project other than the file(s) you worked on. How do you know you didn’t break anything?

Testing.

The Ruby community takes testing very seriously. Testing frameworks for Ruby include Test::Unit, RSpec, Minitest, and Cucumber. Test::Unit was the original standard unit testing framework for Ruby, but it has been deprecated in favor of Minitest.

Typically, tests go in the /test or /spec folder at the root level of the project. Usually, there is a /test/test_helper.rb or /spec/spec_helper.rb that is run before processing the test cases or specifications. This is a good place to configure the global testing environment.

Let’s look at an example of testing with Minitest. Minitest is capable of both unit test and specification-flavoured testing, as well as benchmarking.

Note: Although Ruby ships with Minitest, I got an error from using Minitest::Test instead of Minitest::Unit::TestCase. This was fixed by installing the latest version of the gem.

$ gem install minitest --version 5.4.2

We’ll start with a simple test file using the unit test format.

require "minitest/autorun"

class Foo
  def hello
    "goodbye"
  end
end

class TestFoo < Minitest::Test
  def setup
    @foo = Foo.new
  end

  def test_hello
    assert_equal "hello", @foo.hello
  end
end

If you run the file you should get one failure.

1) Failure:
TestFoo#test_hello [test.rb:15]:
Expected: "hello"
  Actual: "goodbye"

1 tests, 1 assertions, 1 failures, 0 errors, 0 skips

Minitest is cool, but RSpec is arguably the most popular testing framework in the Ruby universe. This is largely thanks to a booming community and various integrations with other testing libraries, like Capybara. Unlike Minitest, RSpec includes a test runner command, rspec, which will check all specs in the spec/ directory hierarchy named *_spec.rb.

First, install the rspec gem.

$ gem install rspec --version 3.1.0

Typically the spec/ directory has a layout like this:

- spec
    - spec_helper.rb  
    - gem_or_app_name
      - models
        - some_model_spec.rb
      - controllers
        - some_controller_spec.rb

For a demo, just create an example spec and a blank spec_helper.rb.

# spec/example_spec.rb
require "spec_helper"

class Foo
  def hello
    "goodbye"
  end
end

describe Foo do
  before(:each) do
    @foo = Foo.new
  end

  it "says hello" do
    expect(@foo.hello).to eq "hello"
  end
end

To check the spec, simply run rspec in the root project directory.

$ rspec

Failures:

  1) Foo says hello
    Failure/Error: expect(@foo.hello).to eq "hello"

      expected: "hello"
            got: "goodbye"

      (compared using ==)
    # ./spec/example_spec.rb:13:in `block (2 levels) in <top (required)>'

Finished in 0.00079 seconds (files took 0.06974 seconds to load)
1 example, 1 failure</top>

What about spec/spec_helper.rb? To see what it’s used for, we’ll add code coverage. SimpleCov is a popular code coverage tool that will generate coverage HTML in the coverage/ directory at the project root.

First, install the simplecov gem.

$ gem install simplecov --version 0.9.1

Now, just call SimpleCov.start in spec/spec_helper.rb.

# spec/spec_helper.rb
require "simplecov"

SimpleCov.start

Coverage will be calculated every time rspec is run. In this case, there’s nothing to cover, but the tool will be activated.

Keep in mind that rspec does not execute spec/spec_helper.rb automatically. It needs to be required within each spec by adding require 'spec_helper' to the top of the spec file.

Summary

Version Management:
– Using the system Ruby installation is fraught with peril – use a version manager instead.
– Use a different gemset for each major project. Isolation can help avoid conflicts as well as problems with dependencies that are specified incorrectly or imprecisely.

Crafting Gems:
– When developing gems, the library directory must be manually added to $LOAD_PATH for code to work as if the gem is installed. This can be done at the command line or programmatically.
File.expand_path('../../lib', __FILE__) refers to a lib folder one directory up, not two directories up as it might seem.
– Only one file, gem_name.rb, should be in the lib directory. The rest of the source code should go lib/gem_name/.
– If using Ruby 1.8.7, require 'rubygems' should go before requiring any gems.
– Gem dependencies can either go in a Bundler Gemfile or a RubyGems gemspec.
– In gemspecs, #executable_files= expects filenames, not paths. A folder named bin is assumed.
– Rakefiles are like Makefiles and define common tasks like testing, building, and releasing.
– It’s popular to use git ls-files to get arrays of files for the gemspec, but Ruby has Dir which is more portable.
bundle exec [command] executes a command using the versions of the gems specified in the Gemfile. This shouldn’t be necessary with RVM or Ruby >= 2.2.0.

Ruby Implementations:
– The reference implementation of Ruby (CRuby / MRI) has a global interpreter lock, so while concurrency is possible with threads, parallelism (multiple processors at once) is not.
– If parallelism is needed in MRI, use forking with Kernel#fork
– Running threads in parallel can be achieved with JRuby, Rubinius, and RubyMotion.

Documentation
– The ri tool can be used to explore core documentation offline.
rdoc is Ruby’s standard embedded documentation generator.
– Documentation for your Ruby implementation may need to be installed by your version manager like with rvm docs generate.
pry is a sophisticated console that can explore documentation and programs at runtime.

Testing
– Tests or specs go in the test/ or spec/ folder of a project.
– Setup and configuration goes in test/test_helper.rb or spec/spec_helper.rb
MiniTest is the standard testing library, but you may encounter legacy code that uses Test::Unit.
– All RSpec specifications can be checked by running rspec in the project root directory containing spec/.
– All specs must end in *_spec.rb or they will be skipped if not run directly.
– RSpec does not execute spec/spec_helper.rb automatically. require 'spec_helper' needs to go in each spec file.
– SimpleCov is a test coverage library and can be used by installing the gem and adding it to the test suite helper file.

Robert QuallsRobert Qualls
View Author

Robert is a voracious reader, Ruby aficionado, and other big words. He is currently looking for interesting projects to work on and can be found at his website.

GlennG
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week