โ† Back to Index

Chapter 7: Libraries & Tools

Bundler, RSpec, Rake & popular Gems

1 Bundler Deep Dive

Bundler manages gem dependencies for Ruby projects. It ensures every developer and every deployment uses the exact same gem versions. Think of it as npm for Ruby.

Gemfile Syntax

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

ruby '~> 3.3.0'

gem 'sinatra',      '~> 4.0'
gem 'activerecord', '~> 7.1'
gem 'pg',           '>= 1.5'
gem 'puma'

gem 'sqlite3', platforms: :ruby

group :development do
  gem 'rubocop',       require: false
  gem 'rubocop-rspec', require: false
end

group :test do
  gem 'rspec',    '~> 3.13'
  gem 'faker'
  gem 'rack-test'
end

group :development, :test do
  gem 'pry'
  gem 'dotenv'
end

Version Constraints

Constraint Meaning Example
'~> 3.0'Pessimistic: >= 3.0, < 4.03.0, 3.1, 3.9 โœ… / 4.0 โŒ
'~> 3.1.2'Pessimistic: >= 3.1.2, < 3.2.03.1.2, 3.1.9 โœ… / 3.2.0 โŒ
'>= 2.0'Any version >= 2.02.0, 5.0, 99.0 โœ…
'= 1.2.3'Exact version only1.2.3 โœ… / 1.2.4 โŒ

Essential Commands

bundle init                    # create new Gemfile
bundle install                 # install all gems
bundle update sinatra          # update specific gem
bundle exec ruby app.rb        # run with bundled gems
bundle exec rspec              # run tests with bundled gems
bundle outdated                # list gems with newer versions
bundle info sinatra            # show gem details
bundle list                    # list all installed gems

The Lock File

# Gemfile.lock records exact versions resolved by Bundler.
# ALWAYS commit Gemfile.lock to version control for applications.
# For gems/libraries, do NOT commit Gemfile.lock.

git add Gemfile Gemfile.lock

โšก bundle exec

Always use bundle exec to run commands within the context of your Gemfile's gems. Without it, Ruby may load system-wide gems instead of the versions specified in your Gemfile.lock. You can add require 'bundler/setup' at the top of your script to auto-activate bundled gems.

๐Ÿ”„ Package Manager Comparison

  • Ruby Bundler (Gemfile / Gemfile.lock) โ‰ˆ Node.js npm (package.json / package-lock.json)
  • Python pip (requirements.txt) โ€” no built-in lock file, use pip-tools or Poetry
  • PHP Composer (composer.json / composer.lock) โ€” directly inspired by Bundler
  • Go modules (go.mod / go.sum) โ€” similar lock file concept

2 RSpec Testing

RSpec is Ruby's most popular testing framework. It uses a BDD (Behavior-Driven Development) style with descriptive syntax that reads like English specifications.

Setup

# Gemfile
gem 'rspec', '~> 3.13', group: :test

bundle install
bundle exec rspec --init
# Creates:
#   .rspec           โ€” default options
#   spec/spec_helper.rb โ€” configuration

Basic Structure: describe / context / it

# lib/calculator.rb
class Calculator
  def add(a, b)
    a + b
  end

  def divide(a, b)
    raise ArgumentError, 'Division by zero' if b.zero?
    a.to_f / b
  end
end
# spec/calculator_spec.rb
require_relative '../lib/calculator'

RSpec.describe Calculator do
  subject(:calc) { described_class.new }

  describe '#add' do
    it 'returns the sum of two numbers' do
      expect(calc.add(2, 3)).to eq(5)
    end

    it 'handles negative numbers' do
      expect(calc.add(-1, -2)).to eq(-3)
    end

    it 'handles zero' do
      expect(calc.add(0, 5)).to eq(5)
    end
  end

  describe '#divide' do
    context 'with valid inputs' do
      it 'returns the quotient' do
        expect(calc.divide(10, 3)).to be_within(0.01).of(3.33)
      end

      it 'returns a float' do
        expect(calc.divide(10, 3)).to be_a(Float)
      end
    end

    context 'when dividing by zero' do
      it 'raises ArgumentError' do
        expect { calc.divide(10, 0) }.to raise_error(ArgumentError, /zero/)
      end
    end
  end
end
bundle exec rspec
# Calculator
#   #add
#     returns the sum of two numbers
#     handles negative numbers
#     handles zero
#   #divide
#     with valid inputs
#       returns the quotient
#       returns a float
#     when dividing by zero
#       raises ArgumentError

Common Matchers

expect(result).to eq(42)             # equality
expect(result).to be > 10            # comparison
expect(result).to be_between(1, 10)  # range
expect(result).to be_nil             # nil check
expect(result).to be_truthy          # truthy value
expect(result).to be_a(String)       # type check
expect(result).to include('hello')   # inclusion
expect(result).to match(/\d{3}/)     # regex
expect(result).to start_with('http') # string prefix
expect(list).to contain_exactly(1, 2, 3)  # array contents (any order)
expect(list).to have_attributes(name: 'Alice')  # object attributes
expect { block }.to change { obj.count }.by(1)  # state change
expect { block }.to raise_error(TypeError)       # exception
expect { block }.to output(/hello/).to_stdout    # stdout

Hooks & Setup

RSpec.describe User do
  before(:all) do
    @db = setup_test_database
  end

  after(:all) do
    @db.close
  end

  before(:each) do
    @user = User.new(name: 'Alice', email: 'alice@example.com')
  end

  after(:each) do
    cleanup_records
  end

  let(:admin) { User.new(name: 'Admin', role: 'admin') }
  let!(:default_user) { User.create!(name: 'Default') }

  it 'has a name' do
    expect(@user.name).to eq('Alice')
  end

  it 'admin has admin role' do
    expect(admin.role).to eq('admin')
  end
end

Mocking & Stubbing

RSpec.describe OrderService do
  describe '#place_order' do
    let(:payment_gateway) { instance_double(PaymentGateway) }
    let(:service) { described_class.new(payment_gateway) }

    it 'charges the payment gateway' do
      allow(payment_gateway).to receive(:charge).and_return(true)

      service.place_order(amount: 99.99)

      expect(payment_gateway).to have_received(:charge).with(99.99)
    end

    it 'handles payment failure' do
      allow(payment_gateway).to receive(:charge)
        .and_raise(PaymentError, 'Declined')

      expect { service.place_order(amount: 99.99) }
        .to raise_error(PaymentError)
    end
  end
end

๐Ÿ’ก let vs before

let is lazy-evaluated (runs only when referenced) and memoized per example. let! is eager (runs before each example). Use let for most setup; use before when you need side effects that aren't captured by a return value.

3 Rake Build Tool

Rake is Ruby's build automation tool โ€” similar to make but written in Ruby. It's used for running tasks, building projects, database migrations, and code generation.

Rakefile Basics

# Rakefile

desc 'Say hello'
task :hello do
  puts 'Hello from Rake!'
end

desc 'Greet someone by name'
task :greet, [:name] do |_t, args|
  name = args[:name] || 'World'
  puts "Hello, #{name}!"
end

task default: :hello
rake hello           # Hello from Rake!
rake greet[Alice]    # Hello, Alice!
rake                 # runs default task
rake -T              # list all tasks with descriptions

Task Dependencies & Namespaces

namespace :db do
  desc 'Create database'
  task :create do
    puts 'Creating database...'
  end

  desc 'Run migrations'
  task migrate: :create do
    puts 'Running migrations...'
  end

  desc 'Seed database'
  task seed: :migrate do
    puts 'Seeding data...'
  end

  desc 'Full database setup'
  task setup: [:create, :migrate, :seed]
end

namespace :assets do
  desc 'Compile assets'
  task :compile do
    puts 'Compiling CSS and JS...'
  end

  desc 'Clean compiled assets'
  task :clean do
    rm_rf 'public/assets'
    puts 'Cleaned.'
  end
end

desc 'Deploy application'
task deploy: ['db:migrate', 'assets:compile'] do
  puts 'Deploying...'
end
rake db:setup          # create โ†’ migrate โ†’ seed
rake db:migrate        # create โ†’ migrate
rake deploy            # db:migrate + assets:compile โ†’ deploy
rake assets:clean

File Tasks

file 'build/app.js' => Dir.glob('src/**/*.js') do |t|
  sh "cat #{t.prerequisites.join(' ')} > #{t.name}"
  puts "Built #{t.name}"
end

directory 'build'
directory 'tmp/cache'

rule '.html' => '.md' do |t|
  sh "pandoc -o #{t.name} #{t.source}"
end

๐Ÿ”„ Build Tool Comparison

  • Ruby Rake โ€” Ruby DSL, dependency-based, ubiquitous in Ruby ecosystem.
  • JavaScript npm scripts โ€” Simple command runners in package.json.
  • Python invoke / Makefile โ€” invoke is Python's Rake equivalent; many projects still use Makefiles.
  • Go Makefile / Mage โ€” Go community primarily uses Makefiles; Mage is a Go-native alternative.

4 Popular Gems

The RubyGems ecosystem has thousands of well-maintained libraries. Here are the essential gems every Ruby developer should know.

Nokogiri โ€” HTML/XML Parsing

require 'nokogiri'
require 'open-uri'

doc = Nokogiri::HTML(URI.open('https://example.com'))

doc.css('h1').each { |h1| puts h1.text }

doc.css('a[href]').each do |link|
  puts "#{link.text.strip} โ†’ #{link['href']}"
end

doc.xpath('//div[@class="content"]//p').each do |p|
  puts p.text
end

fragment = Nokogiri::HTML.fragment('

Hello World

') puts fragment.at('b').text # "World"

Puma โ€” Production Web Server

# config/puma.rb
workers ENV.fetch('WEB_CONCURRENCY', 2)
threads_count = ENV.fetch('RAILS_MAX_THREADS', 5)
threads threads_count, threads_count

preload_app!

port ENV.fetch('PORT', 3000)
environment ENV.fetch('RACK_ENV', 'development')

on_worker_boot do
  ActiveRecord::Base.establish_connection if defined?(ActiveRecord)
end
bundle exec puma -C config/puma.rb

Sidekiq โ€” Background Jobs

class EmailWorker
  include Sidekiq::Job

  def perform(user_id, template)
    user = User.find(user_id)
    Mailer.send(user.email, template)
  end
end

EmailWorker.perform_async(user.id, 'welcome')
EmailWorker.perform_in(1.hour, user.id, 'reminder')

Dry-rb โ€” Functional Programming Toolkit

require 'dry/validation'

class UserContract < Dry::Validation::Contract
  params do
    required(:name).filled(:string, min_size?: 2)
    required(:email).filled(:string, format?: /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i)
    required(:age).filled(:integer, gt?: 0)
  end

  rule(:age) do
    key.failure('must be at least 18') if value < 18
  end
end

contract = UserContract.new
result = contract.call(name: 'A', email: 'bad', age: 15)

unless result.success?
  result.errors.to_h.each do |field, messages|
    puts "#{field}: #{messages.join(', ')}"
  end
end

HTTParty โ€” Simple HTTP Client

require 'httparty'

response = HTTParty.get('https://api.github.com/users/matz',
                        headers: { 'User-Agent' => 'Ruby' })
puts response['name']
puts response['public_repos']

class WeatherAPI
  include HTTParty
  base_uri 'https://api.weatherapi.com/v1'

  def initialize(api_key)
    @options = { query: { key: api_key } }
  end

  def current(city)
    self.class.get('/current.json', @options.merge(query: @options[:query].merge(q: city)))
  end
end

Rails Overview

๐Ÿš‚ Ruby on Rails

Rails is the full-stack web framework that made Ruby famous. It follows the MVC pattern with strong conventions:

  • ActiveRecord โ€” ORM (covered in Chapter 4)
  • ActionController โ€” Request handling and routing
  • ActionView โ€” Template rendering (ERB, Haml)
  • ActiveJob โ€” Background job framework
  • ActionMailer โ€” Email sending
  • ActionCable โ€” WebSocket support
gem install rails
rails new myapp --database=postgresql
cd myapp
rails generate scaffold Post title:string body:text
rails db:migrate
rails server

5 Ruby Code Standards

RuboCop is the standard linter and formatter for Ruby. It enforces the community style guide and catches potential bugs.

Setup & Usage

# Gemfile
gem 'rubocop',       require: false, group: :development
gem 'rubocop-rspec', require: false, group: :development

bundle install
bundle exec rubocop --init    # creates .rubocop.yml
bundle exec rubocop           # check all files
bundle exec rubocop -a        # auto-correct safe issues
bundle exec rubocop -A        # auto-correct all (including unsafe)

Configuration (.rubocop.yml)

# .rubocop.yml
require:
  - rubocop-rspec

AllCops:
  TargetRubyVersion: 3.3
  NewCops: enable
  Exclude:
    - 'vendor/**/*'
    - 'db/schema.rb'
    - 'node_modules/**/*'

Style/Documentation:
  Enabled: false

Style/FrozenStringLiteralComment:
  EnforcedStyle: always

Metrics/MethodLength:
  Max: 20

Metrics/BlockLength:
  Exclude:
    - 'spec/**/*'
    - 'Rakefile'

Layout/LineLength:
  Max: 120

RSpec/ExampleLength:
  Max: 10

Common Rules & Fixes

# frozen_string_literal: true

name = 'Alice'

items = [1, 2, 3]
items.each { |item| puts item }

result = if condition
           'yes'
         else
           'no'
         end

raise ArgumentError, 'invalid input' unless valid?

6 Debugging Tools

Ruby has excellent debugging tools โ€” from simple printing to full interactive debuggers.

pp (Pretty Print)

data = {
  users: [
    { name: 'Alice', roles: [:admin, :editor], settings: { theme: 'dark', lang: 'en' } },
    { name: 'Bob', roles: [:viewer], settings: { theme: 'light', lang: 'ja' } }
  ]
}

p data     # single-line, hard to read
pp data    # formatted, indented output

puts data.inspect

debug Gem (Ruby 3.1+ Built-in)

require 'debug'

def calculate_total(items)
  subtotal = items.sum { |i| i[:price] * i[:qty] }
  binding.break    # debugger stops here
  tax = subtotal * 0.1
  subtotal + tax
end

items = [
  { name: 'Book', price: 29.99, qty: 2 },
  { name: 'Pen',  price: 4.99,  qty: 5 }
]

total = calculate_total(items)
# Debugger commands:
# n (next)      โ€” step over
# s (step)      โ€” step into
# c (continue)  โ€” continue execution
# p expr        โ€” evaluate expression
# info locals   โ€” show local variables
# bt            โ€” backtrace
# q             โ€” quit

Pry โ€” Enhanced REPL & Debugger

require 'pry'

class UserService
  def create(params)
    user = build_user(params)
    binding.pry    # opens Pry REPL at this point
    user.save!
  end
end
# Pry commands:
# ls              โ€” list methods/variables in scope
# cd obj          โ€” change context to object
# show-method m   โ€” show source of method m
# show-doc m      โ€” show documentation
# whereami        โ€” show surrounding code
# exit            โ€” continue execution

๐Ÿ”„ Debugging Tool Comparison

  • Ruby binding.break / binding.pry โ‰ˆ Python breakpoint() / pdb
  • Ruby pp โ‰ˆ Python pprint โ‰ˆ Node.js console.dir
  • Ruby debug gem โ‰ˆ Node.js --inspect โ‰ˆ PHP Xdebug

7 Chapter Summary

๐Ÿ“ฆ Bundler

Dependency management with Gemfile, version constraints (~>), groups, and bundle exec for isolated execution.

๐Ÿงช RSpec

BDD testing with describe/context/it, rich matchers, let/before hooks, and mocking/stubbing.

๐Ÿ”จ Rake

Task automation with dependencies, namespaces, file tasks, and integration with other tools.

๐Ÿ’Ž Popular Gems

Nokogiri (HTML parsing), Puma (web server), Sidekiq (background jobs), Dry-rb (validation), Rails (full-stack).

๐Ÿ‘ฎ RuboCop

Linter and formatter enforcing community standards, with auto-correction and customizable rules via .rubocop.yml.

๐Ÿ› Debugging

pp for quick inspection, debug gem for breakpoints, Pry for interactive exploration.

๐ŸŽ‰ Congratulations! You've completed the Ruby Quick Tutorial! You now have a solid foundation in Ruby โ€” from basic syntax to databases, networking, file operations, and the tooling ecosystem. The next step is to build real projects: try creating a Sinatra API, a CLI tool with Thor, or dive into Ruby on Rails.