← 返回目录

第七章:常用库与工具

Bundler、RSpec、Rake 与热门 Gem

1 Bundler 深入

Bundler 是 Ruby 的依赖管理工具,通过 Gemfile 声明项目依赖,通过 Gemfile.lock 锁定精确版本,确保团队和部署环境的一致性。

Gemfile 语法

source 'https://rubygems.org'

ruby '~> 3.3.0'

gem 'rails', '~> 7.1'
gem 'pg', '>= 1.5'
gem 'puma', '~> 6.0'
gem 'sidekiq'
gem 'faraday', '~> 2.0'

gem 'nokogiri', require: false
gem 'my_gem', path: '../my_gem'
gem 'private_gem', git: 'https://github.com/org/private_gem.git', branch: 'main'

group :development do
  gem 'rubocop', require: false
  gem 'solargraph'
  gem 'debug'
end

group :test do
  gem 'rspec', '~> 3.12'
  gem 'factory_bot'
  gem 'faker'
  gem 'simplecov', require: false
end

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

group :production do
  gem 'redis', '~> 5.0'
end

版本约束语法

写法 含义 匹配范围
'~> 2.1'悲观约束(推荐)>= 2.1, < 3.0
'~> 2.1.3'悲观约束(补丁级)>= 2.1.3, < 2.2.0
'>= 1.0'大于等于1.0 及以上所有版本
'2.1.0'精确版本仅 2.1.0
(无版本)任意版本最新兼容版本

常用命令

# 初始化
bundle init

# 安装依赖(首次或 Gemfile 变更后)
bundle install

# 更新特定 gem
bundle update faraday

# 更新所有 gem(谨慎使用)
bundle update

# 在 bundle 环境中执行命令
bundle exec rspec
bundle exec rake db:migrate

# 查看过期的 gem
bundle outdated

# 查看 gem 安装位置
bundle show faraday

# 添加 gem(自动写入 Gemfile)
bundle add sidekiq

# 移除 gem
bundle remove sidekiq

# 检查安全漏洞
bundle audit

⚡ Gemfile.lock 的重要性

  • 必须提交到 Git(应用项目)——确保团队和 CI/CD 使用完全相同的版本
  • Gem 库项目则不提交 Gemfile.lock——让使用者自行解析依赖
  • bundle install 优先使用 lock 文件中的版本,bundle update 才会更新

2 RSpec 测试框架

RSpec 是 Ruby 社区最流行的测试框架,采用 BDD(行为驱动开发)风格,测试代码读起来像自然语言描述。

gem install rspec
rspec --init    # 生成 .rspec 和 spec/spec_helper.rb

基础结构

# spec/calculator_spec.rb
require_relative '../calculator'

RSpec.describe Calculator do
  describe '#add' do
    it '返回两个数的和' do
      calc = Calculator.new
      expect(calc.add(2, 3)).to eq(5)
    end

    it '处理负数' do
      calc = Calculator.new
      expect(calc.add(-1, -2)).to eq(-3)
    end

    it '处理浮点数' do
      calc = Calculator.new
      expect(calc.add(0.1, 0.2)).to be_within(0.001).of(0.3)
    end
  end

  describe '#divide' do
    it '返回两个数的商' do
      calc = Calculator.new
      expect(calc.divide(10, 3)).to eq(3)
    end

    context '当除数为零时' do
      it '抛出 ZeroDivisionError' do
        calc = Calculator.new
        expect { calc.divide(10, 0) }.to raise_error(ZeroDivisionError)
      end
    end
  end
end

常用 Matchers

expect(value).to eq(expected)
expect(value).to eql(expected)
expect(value).to be(expected)
expect(value).to equal(expected)

expect(value).to be > 5
expect(value).to be_between(1, 10).inclusive
expect(value).to be_within(0.1).of(3.14)

expect(string).to include('hello')
expect(string).to start_with('Hello')
expect(string).to end_with('world')
expect(string).to match(/\d{3}-\d{4}/)

expect(array).to include(1, 3)
expect(array).to contain_exactly(3, 1, 2)
expect(array).to have_attributes(size: 3)
expect(hash).to include(name: '张三')

expect(value).to be_nil
expect(value).to be_truthy
expect(value).to be_falsey
expect(value).to be_a(String)
expect(value).to respond_to(:length)

expect { dangerous_operation }.to raise_error(RuntimeError, /失败/)
expect { array.push(1) }.to change { array.size }.by(1)
expect { puts 'hello' }.to output(/hello/).to_stdout

before/after 与 let

RSpec.describe User do
  let(:user) { User.new(name: '张三', email: 'zhangsan@example.com') }
  let!(:admin) { User.create!(name: 'Admin', role: :admin) }

  before(:each) do
    DatabaseCleaner.start
  end

  after(:each) do
    DatabaseCleaner.clean
  end

  before(:all) do
    @shared_resource = ExpensiveSetup.new
  end

  describe '#valid?' do
    it '有效用户返回 true' do
      expect(user).to be_valid
    end

    context '缺少姓名时' do
      let(:user) { User.new(name: '', email: 'test@example.com') }

      it '返回 false' do
        expect(user).not_to be_valid
        expect(user.errors[:name]).to include("不能为空")
      end
    end
  end
end

Mocking 与 Stubbing

RSpec.describe OrderService do
  describe '#place_order' do
    it '发送通知邮件' do
      mailer = instance_double(OrderMailer)
      allow(OrderMailer).to receive(:new).and_return(mailer)
      expect(mailer).to receive(:send_confirmation).with(order_id: 123)

      service = OrderService.new
      service.place_order(order_id: 123)
    end

    it '调用支付网关' do
      gateway = double('PaymentGateway')
      allow(gateway).to receive(:charge).and_return({ status: 'success', id: 'txn_123' })

      service = OrderService.new(payment_gateway: gateway)
      result = service.place_order(amount: 99.99)

      expect(result[:status]).to eq('success')
      expect(gateway).to have_received(:charge).with(amount: 99.99)
    end
  end
end
# 运行测试
bundle exec rspec

# 运行特定文件
bundle exec rspec spec/models/user_spec.rb

# 运行特定行
bundle exec rspec spec/models/user_spec.rb:15

# 只运行失败的测试
bundle exec rspec --only-failures

# 格式化输出
bundle exec rspec --format documentation

3 Rake 构建工具

Rake 是 Ruby 版的 Make——用 Ruby 语法定义任务和依赖关系。Rails 项目的 rake db:migraterake routes 等命令都基于 Rake。

Rakefile 基础

# Rakefile

desc '打招呼'
task :hello do
  puts 'Hello from Rake!'
end

desc '显示当前时间'
task :time do
  puts Time.now
end

task default: :hello
rake hello    # 运行 hello 任务
rake time     # 运行 time 任务
rake          # 运行默认任务(hello)
rake -T       # 列出所有任务

任务依赖

desc '准备环境'
task :setup do
  puts '安装依赖...'
  sh 'bundle install'
end

desc '运行测试'
task test: :setup do
  sh 'bundle exec rspec'
end

desc '代码检查'
task lint: :setup do
  sh 'bundle exec rubocop'
end

desc '完整 CI 流程'
task ci: [:lint, :test] do
  puts '✅ CI 通过!'
end

命名空间

namespace :db do
  desc '创建数据库'
  task :create do
    puts '创建数据库...'
  end

  desc '运行迁移'
  task migrate: :create do
    puts '运行数据库迁移...'
  end

  desc '填充种子数据'
  task seed: :migrate do
    puts '填充种子数据...'
  end

  desc '重置数据库(删除 + 创建 + 迁移 + 种子)'
  task reset: [:create, :migrate, :seed]
end

namespace :assets do
  desc '编译资源文件'
  task :compile do
    puts '编译 CSS/JS...'
  end

  desc '清理编译产物'
  task :clean do
    rm_rf 'public/assets'
    puts '已清理'
  end
end
rake db:migrate
rake db:reset
rake assets:compile

带参数的任务

desc '部署到指定环境'
task :deploy, [:env] do |t, args|
  env = args[:env] || 'staging'
  puts "部署到 #{env} 环境..."
  sh "cap #{env} deploy"
end

namespace :user do
  desc '创建管理员'
  task :create_admin, [:name, :email] do |t, args|
    puts "创建管理员: #{args[:name]} (#{args[:email]})"
  end
end
rake deploy[production]
rake user:create_admin[张三,zhangsan@example.com]

🔄 构建工具对比

工具 语言 配置文件 特点
RakeRubyRakefile纯 Ruby 语法,表达力强
MakeC/通用Makefile经典工具,Tab 缩进要求
GradleJava/Kotlinbuild.gradleGroovy/Kotlin DSL,功能强大
npm scriptsNode.jspackage.jsonJSON 配置,Shell 命令

4 热门 Gem 推荐

🔍 Nokogiri — HTML/XML 解析

require 'nokogiri'
require 'open-uri'

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

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

links = doc.css('a[href]').map { |a| a['href'] }

items = doc.xpath('//div[@class="item"]')
items.each do |item|
  title = item.at_css('.title')&.text
  price = item.at_css('.price')&.text
  puts "#{title}: #{price}"
end

🚀 Puma — 高性能 Web 服务器

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

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

preload_app!

on_worker_boot do
  ActiveRecord::Base.establish_connection
end
# 启动
bundle exec puma -C config/puma.rb

⚡ Sidekiq — 后台任务

class ReportWorker
  include Sidekiq::Job
  sidekiq_options queue: 'default', retry: 3

  def perform(user_id, report_type)
    user = User.find(user_id)
    report = ReportGenerator.new(user)
    report.generate(report_type)
    ReportMailer.send(user, report)
  end
end

ReportWorker.perform_async(42, 'monthly')

ReportWorker.perform_in(1.hour, 42, 'daily')

🛤️ Rails — 全栈框架概览

# 创建新项目
rails new myapp --database=postgresql

# 生成资源
rails generate scaffold Post title:string body:text

# 数据库操作
rails db:create db:migrate db:seed

# 启动服务器
rails server

# 控制台
rails console

💧 Dry-rb — 函数式工具集

require 'dry/validation'

class UserContract < Dry::Validation::Contract
  params do
    required(:name).filled(:string, min_size?: 2)
    required(:email).filled(:string, format?: /@/)
    required(:age).filled(:integer, gteq?: 18)
  end

  rule(:email) do
    key.failure('已被注册') if User.exists?(email: value)
  end
end

result = UserContract.new.call(
  name: '张三', email: 'zhangsan@example.com', age: 28
)
puts result.success?
puts result.errors.to_h

📡 HTTParty — 简易 HTTP

require 'httparty'

response = HTTParty.get('https://api.example.com/users')
puts response.parsed_response

response = HTTParty.post(
  'https://api.example.com/users',
  body: { name: '张三', email: 'z@example.com' }.to_json,
  headers: { 'Content-Type' => 'application/json' }
)

class GitHub
  include HTTParty
  base_uri 'https://api.github.com'
  headers 'User-Agent' => 'Ruby HTTParty'

  def self.user(username)
    get("/users/#{username}")
  end
end

puts GitHub.user('matz')['name']

5 Ruby 代码规范

RuboCop 是 Ruby 社区的标准代码检查和格式化工具,基于 Ruby Style Guide 实施代码规范。

gem install rubocop

# 检查当前目录
rubocop

# 自动修复可安全修复的问题
rubocop -a

# 激进自动修复(可能改变行为,需 review)
rubocop -A

# 只检查特定文件
rubocop app/models/user.rb

# 生成配置文件
rubocop --init

.rubocop.yml 配置

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

Style/StringLiterals:
  EnforcedStyle: single_quotes

Style/Documentation:
  Enabled: false

Metrics/MethodLength:
  Max: 20

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

Layout/LineLength:
  Max: 120

Style/FrozenStringLiteralComment:
  EnforcedStyle: always

💡 Ruby 代码风格要点

  • 缩进使用 2 空格(非 Tab),这是 Ruby 社区共识
  • 方法名和变量名用 snake_case,类名用 CamelCase
  • 谓词方法以 ? 结尾(如 empty?),危险方法以 ! 结尾(如 save!
  • 字符串默认使用单引号,需要插值时用双引号

6 调试工具

debug gem(Ruby 3.1+ 内置)

require 'debug'

def calculate_total(items)
  subtotal = items.sum { |item| item[:price] * item[:quantity] }
  binding.break
  tax = subtotal * 0.1
  subtotal + tax
end

items = [
  { name: 'Ruby 书', price: 59.9, quantity: 2 },
  { name: '键盘', price: 299.0, quantity: 1 },
]
total = calculate_total(items)
# 调试器命令
(rdbg) n          # next - 执行下一行
(rdbg) s          # step - 步入方法
(rdbg) c          # continue - 继续执行
(rdbg) p subtotal # print - 打印变量
(rdbg) pp items   # pretty print
(rdbg) info       # 显示局部变量
(rdbg) bt         # backtrace
(rdbg) watch @total # 监视变量变化
(rdbg) break user.rb:25 # 设置断点
(rdbg) q          # 退出

pp 与 p 输出调试

user = { name: '张三', roles: [:admin, :editor], settings: { theme: 'dark', lang: 'zh' } }

p user
pp user
puts user.inspect

users = User.where('age > 25').limit(5)
pp users.map { |u| [u.id, u.name, u.email] }

Pry 交互式控制台

gem install pry pry-byebug
require 'pry'

def process_order(order)
  total = order.items.sum(&:price)
  binding.pry
  apply_discount(total, order.coupon)
end
# Pry 命令
pry(main)> ls              # 列出当前作用域的方法和变量
pry(main)> cd User         # 进入 User 类的上下文
pry(main)> show-method initialize  # 查看方法源码
pry(main)> show-doc Array#map     # 查看文档
pry(main)> whereami        # 显示当前位置
pry(main)> next            # 下一行(pry-byebug)
pry(main)> step            # 步入
pry(main)> continue        # 继续

💡 调试策略

  • 快速检查:p/pp 输出变量值,适合简单问题
  • 交互式探索:binding.prybinding.break,暂停执行后自由探索
  • 生产环境:使用日志(Logger)而非调试器,结合错误追踪服务(Sentry/Honeybadger)
  • 性能调试:benchmark 标准库,或 stackprof/ruby-prof gem

7 本章要点

📦 Bundler

Gemfile 声明依赖,Gemfile.lock 锁定版本;~> 悲观约束最常用

🧪 RSpec

BDD 风格测试;describe/context/it 组织,expect().to 断言,let 惰性求值

🔨 Rake

Ruby 构建工具;task 定义任务,namespace 组织,支持依赖和参数

💎 热门 Gem

Nokogiri 解析 HTML、Puma 服务器、Sidekiq 后台任务、Rails 全栈框架

📏 RuboCop

代码规范检查与自动格式化;-a 安全修复,.rubocop.yml 自定义规则

🐛 调试

debug gem 内置断点调试;Pry 交互式控制台;p/pp 快速输出

🎉 恭喜完成 Ruby 速成教程!

你已经掌握了 Ruby 3.3 的核心知识——从环境搭建、基础语法、方法与面向对象、数据库操作、网络通信、文件处理,到 Bundler 生态与常用工具库。

下一步建议:

  1. 用 Sinatra 或 Rails 构建一个完整的 Web 应用项目
  2. 为项目编写 RSpec 测试,养成测试驱动的习惯
  3. 阅读 Ruby Style Guide,了解社区最佳实践
  4. 探索 RubyGems.org 上的更多开源 Gem