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.0 | 3.0, 3.1, 3.9 โ / 4.0 โ |
| '~> 3.1.2' | Pessimistic: >= 3.1.2, < 3.2.0 | 3.1.2, 3.1.9 โ / 3.2.0 โ |
| '>= 2.0' | Any version >= 2.0 | 2.0, 5.0, 99.0 โ |
| '= 1.2.3' | Exact version only | 1.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, usepip-toolsorPoetry - 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 โ
invokeis 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.