Ruby

例外処理とテスト

Ruby

begin/rescue・raise・RSpec・minitest

例外処理

begin/rescue/ensure/raise・カスタム例外

exceptions.rb ruby
# 基本構文
begin
  result = 10 / 0
rescue ZeroDivisionError => e
  puts "エラー: #{e.message}"
rescue TypeError, ArgumentError => e
  puts "型・引数エラー: #{e.message}"
rescue StandardError => e
  puts "その他のエラー: #{e.class}: #{e.message}"
else
  puts "成功: #{result}"  # 例外がなかった場合
ensure
  puts '常に実行(リソース解放など)'
end

# メソッド内では begin/end を省略できる
def parse_json(str)
  JSON.parse(str)
rescue JSON::ParserError => e
  logger.error "JSON パースエラー: #{e.message}"
  nil
end

# 例外の発生
raise ArgumentError, '引数が不正です'
raise RuntimeError.new('カスタムメッセージ')
raise  # 現在の例外を再送出(rescue 内で)

# カスタム例外クラス
class AppError < StandardError
  attr_reader :code

  def initialize(message = 'アプリエラー', code: 500)
    super(message)  # StandardError の initialize に渡す
    @code = code
  end
end

class AuthenticationError < AppError
  def initialize(msg = '認証失敗')
    super(msg, code: 401)
  end
end

class NotFoundError < AppError
  def initialize(resource = 'リソース')
    super("#{resource}が見つかりません", code: 404)
  end
end

# 使用
begin
  raise NotFoundError.new('ユーザー')
rescue NotFoundError => e
  puts "#{e.message} (#{e.code})"  # 'ユーザーが見つかりません (404)'
rescue AppError => e
  puts "アプリエラー: #{e.code}"
end

# retry
attempts = 0
begin
  attempts += 1
  flaky_network_call()
rescue NetworkError => e
  retry if attempts < 3
  raise  # 3回失敗したら再送出
end

RSpec

describe・it・expect・matcher・mock

spec/user_spec.rb ruby
require 'spec_helper'

RSpec.describe User do
  # shared setup
  let(:user) { User.new(name: 'Alice', age: 30) }  # 遅延評価
  let!(:saved_user) { create(:user) }               # 即時評価

  subject { user }  # it { is_expected.to ... } で使える

  describe '#name' do
    it 'returns the name' do
      expect(user.name).to eq('Alice')
    end
  end

  describe '#adult?' do
    context 'when age is 18 or over' do
      it { is_expected.to be_adult }
    end

    context 'when age is under 18' do
      let(:user) { User.new(name: 'Bob', age: 17) }
      it { is_expected.not_to be_adult }
    end
  end

  # よく使うマッチャー
  it 'demonstrates matchers' do
    expect(1 + 1).to eq(2)                     # 等値
    expect('hello').to include('ell')           # 包含
    expect([1,2,3]).to contain_exactly(3,2,1)  # 順不同で等値
    expect(3.14).to be_within(0.01).of(Math::PI) # 近似値
    expect { raise 'err' }.to raise_error(RuntimeError, 'err')  # 例外
    expect { user.save }.to change(User, :count).by(1)  # 変化量
    expect(user).to have_attributes(name: 'Alice', age: 30)
    expect(nil).to be_nil
    expect([]).to be_empty
    expect(5).to be_between(1, 10).inclusive
  end

  # モックとスタブ
  it 'sends a welcome email' do
    mailer = instance_double(UserMailer)  # 型チェックあり
    allow(UserMailer).to receive(:new).and_return(mailer)
    allow(mailer).to receive(:welcome)

    user.send_welcome_email

    expect(mailer).to have_received(:welcome).once
  end

  # shared examples
  shared_examples 'a valid entity' do
    it { is_expected.to be_valid }
    it { expect(subject.errors).to be_empty }
  end

  include_examples 'a valid entity'
end

Minitest

テストクラス・アサーション・モック

test/user_test.rb ruby
require 'minitest/autorun'
require 'minitest/mock'

class UserTest < Minitest::Test
  def setup
    @user = User.new(name: 'Alice', age: 30)  # 各テスト前に実行
  end

  def teardown
    # 後片付け(必要な場合)
  end

  def test_name
    assert_equal 'Alice', @user.name
  end

  def test_adult
    assert @user.adult?,         '成人のはず'
    refute User.new(name:'Bob', age:17).adult?, '未成年のはず'
  end

  # よく使うアサーション
  def test_assertions
    assert_equal    42,     value            # ==
    assert_nil              value            # nil
    assert_empty            collection       # 空
    assert_includes [1,2,3], 2              # 包含
    assert_match    /hello/, string         # 正規表現
    assert_in_delta 3.14, Math::PI, 0.01   # 近似値
    assert_raises(ArgumentError) { risky } # 例外
    assert_respond_to @user, :name          # メソッド存在
  end

  # モック
  def test_sends_email
    mock_mailer = Minitest::Mock.new
    mock_mailer.expect(:welcome, nil)  # welcomeが呼ばれることを期待

    UserMailer.stub(:new, mock_mailer) do
      @user.send_welcome_email
    end

    mock_mailer.verify
  end
end

# Spec スタイル(RSpec 風)
describe User do
  it 'has a name' do
    user = User.new(name: 'Alice')
    _(user.name).must_equal 'Alice'
    _(user).must_respond_to :adult?
  end
end