Techioz Blog

Rubyで一時関数を定義するにはどうすればよいですか?

概要

テストやその他の場合に繰り返しコードを記述することは避けたいと考えています。

不便なドット呼び出し構文test_parse_tag.(…)でラムダを使用する以外に良い方法はありますか?

def test(name) = yield
def assert_equal(expected, actual) = p :tested

class El
  def self.parse_tag(s) = [:div, { class: 'some' }]
end

# I don't want to type `assert_equal El.parse_tag` every time
test 'El.parse_tag' do
  assert_equal El.parse_tag('span'),      [:span, {}]
  assert_equal El.parse_tag('#id'),       [:div,  { id: 'id' }]
  assert_equal El.parse_tag('div  a=b'),  [:div,  { a: 'b' }]
end

# So I use shortcut `test_parse_tag`
test 'El.parse_tag' do
  def test_parse_tag(actual, expected)
    assert_equal El.parse_tag(actual), expected
  end

  test_parse_tag 'span',      [:span, {}]
  test_parse_tag '#id',       [:div,  { id: 'id' }]
  test_parse_tag 'div  a=b',  [:div,  { a: 'b' }]
end

# PROBLEM, I don't want `test_parse_tag` to be available here
test_parse_tag 'span', [:span, {}]

解決策

事前に作成したオブジェクトのコンテキストで、テストに渡されたブロックを実行できます。以下は、必要に応じてオブジェクトをブロック引数として渡す、instance_exec を使用した単純なバージョンです。

def test(name, &block)
  obj = Object.new
  obj.instance_exec(obj, &block)
end

オブジェクト内でブロックを実行すると、メソッドはオブジェクトのシングルトン クラスで定義され、外部に漏れることはありません。

test 'one' do |context|
  def foo ; end

  defined? foo       #=> "method"
  context            #=> <Object:0x00007fd3b002c2f0>
  method(:foo).owner #=> #<Class:#<Object:0x00007fd3b002c2f0>>
end

test 'two' do |context|
  defined? foo       #=> nil
  context            #=> #<Object:0x00007fd3b0026fd0>
end

defined? foo #=> nil

RSpec はこのアプローチをさらに一歩進めています。各サンプル グループがクラスを作成し、各サンプルがそのクラスのインスタンス内で評価されます。スコープについての説明を参照してください。