ずみーBlog

元クラゲ研究者(見習い)の92年生まれがエンジニアを目指しながら日々寄り道するブログです。

【Ruby】sendメソッドとattr_accessorで属性を動的に読み書きする

おはようございます。ずみーです。 Rubyでメソッドの呼び出しはインスタンス名.メソッド名ですが、インスタンス名.メソッド名の部分をパラメータにして動的に呼び出したいことってありますよね。

結論:sendメソッドで実現可能

使い方はこんな感じです。

#インスタンスobjとメソッド名methodを受け取ってメソッドを呼び出す関数
def execute_method(obj, method)
  obj.send(method)
end

また、インスタンスobjattr_accesorで属性が定義してある場合、属性への値の書き込みは以下のようにやります。

#クラス
class ClassName
  attr_accessor :attr1, :attr2
end

#インスタンスobjと属性名attrと値valを受け取って、属性値を設定する関数
def set_attr(obj, attr, val)
  obj.send("#{attr}=", val)
end

#呼び出しかた
instance = new ClassName
set_attr(instance, "attr1", "val1")
set_attr(instance, "attr2", "val2")

どこで使う?

私はFormオブジェクトパターンでの単体テストで使いました。

単体テストの行数を少しでも減らすため、インスタンスと属性を受け取って、「presence: true」のバリデーションをテストするヘルパー関数を定義しました。

モデル

Formオブジェクトパターン(参考記事)で定義したモデルクラスです。

saveメソッドで複数テーブルにレコード作成できるようにしています。

app/models/transaction.rb

class Transaction
  include ActiveModel::Model
  attr_accessor :user, :item, :token, :postal_code, :prefecture_id, :city, :addresses, :building, :phone_number
  # belongs_to_active_hash :prefecture

  POSTAL_CODE_REGEX = /\A\d{3}[-]\d{4}\z/.freeze
  PHONE_NUMBER_REGEX = /\A\d{1,11}\z/.freeze

  with_options presence: true do
    validates :user
    validates :item
    validates :token, presence: { message: 'を正しく入力してください' }
    validates :postal_code, format: { with: POSTAL_CODE_REGEX, message: 'はハイフン付きで000-0000の形式で入力してください' }
    validates :prefecture_id, numericality: { other_than: 0, message: 'を選択してください' }
    validates :city
    validates :addresses
    validates :phone_number, format: { with: PHONE_NUMBER_REGEX, message: 'は11桁以内の半角数字で入力してください' }
  end

  def save
    order = Order.create(user: user, item: item)
    Postal.create(order_id: order.id, postal_code: postal_code, prefecture_id: prefecture_id, city: city, addresses: addresses, building: building, phone_number: phone_number)
  end
end

単体テスト用ヘルパーメソッド(RSpec使用)

インスタンスと属性と属性の日本語訳をパラメータとして受け取り、属性をnilにしてバリデーションをテストするヘルパー関数がこちらです。 上で書いた通り、sendメソッドを使って、objcol属性にnilを代入し、バリデーションをテストしています。 今回の環境ではデフォルトのロケール:jaだったため、エラーメッセージは"#{nihongo}を入力してください"となります(nihongoは属性名の日本語訳)。日本語訳の取得方法はこちらの記事にの追記に書きました。

spec/support/basic_validation_support.rb

module BasicValidationSupport
  def not_nil_validation_dynamic(obj, col, nihongo)
    obj.send("#{col}=", nil)
    obj.valid?
    expect(obj.errors.full_messages).to include("#{nihongo}を入力してください")
  end
end

RSpecでヘルパーメソッドを有効化する方法についてはこちらをご覧ください。

単体テストコード

FactoryBotとFakerを使って書いています。 属性がnilなことが原因でinvalidになるテストパターンで、テストしたい属性を一つずつ上のヘルパーメソッドに渡して、動的にテストしています。

spec/models/transaction_spec.rb

require 'rails_helper'

RSpec.describe Transaction, type: :model do
  before do
    @transaction = FactoryBot.build(:transaction)
  end

  describe '購入手続きをする(注文レコードと住所レコードを新規作成する)' do
    #省略
    context '購入手続きができないとき' do
      #省略
      context '属性の欠損が原因のとき' do
        columns = %w[user item postal_code prefecture_id city addresses phone_number]
        columns.each do |col|
          nihongo = Transaction.human_attribute_name(col)
          it "#{nihongo}が無ければ登録できない" do
            not_nil_validation_dynamic(@transaction, col, nihongo)
          end
        end
      end
      #省略
    end
  end
end

実行結果

Transaction
  購入手続きをする(注文レコードと住所レコードを新規作成する)
    購入手続きができるとき
      #省略
        購入者が無ければ登録できない
        商品が無ければ登録できない
        郵便番号が無ければ登録できない
        都道府県が無ければ登録できない
        市区町村が無ければ登録できない
        番地が無ければ登録できない
        電話番号が無ければ登録できない

Finished in 0.11757 seconds (files took 2.36 seconds to load)
9 examples, 0 failures