例外処理とテスト
Ruby
begin/rescue・raise・RSpec・minitest
例外処理
begin/rescue/ensure/raise・カスタム例外
# 基本構文
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回失敗したら再送出
endRSpec
describe・it・expect・matcher・mock
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'
endMinitest
テストクラス・アサーション・モック
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