DelegateClass に起因するスペックテスト問題

私の大好きな Bacon さん(一応 RSpec クローンですけど、記法が色々違うので注意して下さい)で DelegateClass を使って定義したクラスのオブジェクトの振舞いを定めていて気付きました。RSpec についても同様の状況ですので、DelegateClass を使う場合には注意しましょう。次のような例をご覧下さい。

require "delegate"

class A; end
class B < DelegateClass(A)
  def initialize
    super(A.new)
  end
end

describe "class B" do
  it 'should be kind of B' do
    B.new.kind_of?(B).should.true
    B.new.should.kind_of B
  end
end

さて、このスペックテストの結果はどうなるでしょうか。B のインスタンスは当然 B の一種でしょ!? と考えると痛い目にあいます。これを 1.8 系で実行してみると次のようになります。

class B
- should be kind of B [FAILED]

Bacon::Error: #<A:0xb75809dc>.kind_of?(B) failed
	/home/keita/Build/test.rb:13: class B - should be kind of B
	/home/keita/Build/test.rb:11
	/home/keita/Build/test.rb:10

1 specifications (2 requirements), 1 failures, 0 errors

以上は、

  • B.new.kind_of?(B).should.true は成功
  • B.new.should.kind_of B は失敗

という結果となったことを示しています。あれ変だな、と思いませんでしょうか。B.new.kind_of?(B) は真であるというのに、B.new.should.kind_of(B) は失敗するのです。これはなぜでしょうか。

まず、直接の原因を探るために、bacon.rb で定義されている Object#should を見ておくことにします。

class Object
  def should(*args, &block)    Should.new(self).be(*args, &block)         end
end

つまり、B.new.should は B のインスタンスに #should を送るわけですが、これは DelegateClass の作用によって A のインスタンスにフックされてしまいます。従って実際には A.new.should となり、定義に従って Should.new(A.new).be 即ち Should インスタンスが返されます(be メソッドは基本的に何もせずに自分自身を返します)。これにより B.new.should.kind_of B は実際には Should.new(A.new).kind_of B と同等であることが分かります。よってこのスペックテストは失敗に終わるわけです。

だから、こうした DelegateClass にかかわるスペックを書く時には、次のように直接 Should クラスからオブジェクトを作成するというバッドノウハウを蛮行すれば良いだろうと思われます。

require "delegate"

class A; end
class B < DelegateClass(A)
  def initialize
    super(A.new)
  end
end

describe "class B" do
  it 'should be kind of B' do
    B.new.kind_of?(B).should.true
    Should.new(B.new).kind_of B
  end
end

このスペックの結果は 1.8 系では次のようになります。

class B
- should be kind of B

1 specifications (2 requirements), 0 failures, 0 errors

というわけで成功しました。ああ良かった素敵なバッドノウハウでいともたやすく解決だね!などと安堵していると血反吐を吐いて死にます。ではこれを 1.9 系で実行してみましょう。

class B
- should be kind of B [FAILED]

Bacon::Error: false.true?() failed
	/home/keita/Build/test.rb:12:in `block (2 levels) in <top (required)>': class B - should be kind of B
	/home/keita/Build/test.rb:11:in `block in <top (required)>'
	/home/keita/Build/test.rb:10:in `<top (required)>'

1 specifications (1 requirements), 1 failures, 0 errors

これは B.new.kind_of?(B).should.true の失敗を指し示しています。え?と思うかも知れませんが、実は DelegateClass の振舞は 1.8 系と 1.9 系でかなり違います。だから本当は 1.9 系では次のようなスペックを書くべきなのです。

require "delegate"

class A; end
class B < DelegateClass(A)
  def initialize
    super(A.new)
  end
end

describe "class B" do
  it 'should be kind of B' do
    B.new.kind_of?(B).should.false
    Should.new(B.new).kind_of A
  end
end

1.9系における結果は次のようになります。

class B
- should be kind of B

1 specifications (2 requirements), 0 failures, 0 errors

というわけで、 B.new.kind_of?(B) は偽だし、Should.new(B.new).kind_of A は正しいのです。このようにして最初に述べた「B のインスタンスは当然 B の一種でしょ!?」という直観はいともたやすく打ち砕かれてしまったわけです。

以上から分かる通り 1.9 系における DelegateClass はメソッドの委譲具合が半端じゃありません。#kind_of? だけではなく例えば #class なども委譲してしまいますから、これは注意が必要です。つまりどういうことかと言うと self.class.new とかするとハマるよ!ってことです。DelegateClass には魔物が潜んでいるので、これについてはまた別に書きます。

まぁこのように大袈裟に書きたてたところですけれども、実際のところ、スペックにはもっと振舞いらしいところを書くようにすればいいだけです。kind_of? とか書く必要は実際のところあまりないでしょう(でも私は書きます)。だからそういうわけでこの記事は DelegateClass の怪しさを喧伝するためだけに書いたのでした!

ていうかそもそも delegation ってなに?という文系らしい初歩的な疑問を追い掛けて、現在 Self にまでようやく漕ぎ着けたところなのですが、行き着く先が Self で本当に大丈夫なのでしょうか。ところで Self って名前しか知らなかったけど、やったねプロトタイプベースってヤツですね!まだ開発続いてるような雰囲気でなによりです。