前々からCocoaで開発してるときにTDDがいまひとつやりにくいのが気になってた。Xcodeにも組み込まれててデファクトになってるOCUnit、あれが結構使いにくい。単に使いかたを理解してないだけだという気もするけど、それを差し引いてもTDDのツールとしてはそんなに洗練されてないと思う。実行結果が視覚的に面白くないし(これはモチベーション下るので結構重要)、エラーの通知もあんまり解析しやすくない。そういえばアサーションの実態がCのマクロなのでObjective-Cの文法じゃないのも気になるし、あんまり柔軟じゃない。他にもいろいろな実装はあるらしいけど、ただでさえ情報の少ないCocoaのそれもTDDのしかも代替フレームワークともなると、日本語で読める最新の詳細な情報など皆無。ここでぬぬぬぬ、となるわけですよ。

なきゃ作れという話

ということで、最初は自分で作ることを試みた。もっとわかりやすいのがいいし、せっかくObjective-Cみたいな動的な言語なんだからリフレクションとか使って柔軟にやって欲しいし、ああどうせならBDDっぽい語彙で書きたいよなー、とか妄想を膨らます。イメージとしては

- (void) before {
  // 俺を作る
  faul = [[Namakemono alloc] initWithName:@"faultier"];
}

- (void) after {
  // 俺を逃がしてやる
  [faul release];
}

- (void) whenIntroduce {
  [faul shouldKindOf:Animal];
  [faul shouldKindOf:Namakemono];
  [[faul name] shouldEqual:@"faultier"];
  [[faul capitarizedName] shouldEqual:@"Faultier"];
}

- (void) whenStudy {
  // 来週はテストだ
  id study = [[Task alloc] initWithName:@"English Study"];
  [faul setTask:study key:@"study"];

  [[faul task:@"study"] shuldEqual:study];
  [faul shouldThrow:NullYarukiException message:@selector(reportProgress:) arguments:@"study"];

  // studyタスクを開放し忘れるとテスト直前にメモリリークしてることに気付く…
  [study release];
}

こんな感じ。あとモックも欲しいなぁ、CUI版とXcode組み込み版のRunnerはそれぞれいるよなぁ、とか言ってたら手に負えなくなってきた。あは。

じゃあRubyで書けばいいんじゃね?

RubyにはRSpecという素敵な代物が既にある。スクリプトなら拡張も容易だし、Rubyの表現力なら好き放題書ける。RubyCocoaはLeopardでは標準で入るらしいから環境整える手間もいらないし、それを使えばサクっとBDDできるんじゃない?と思い立ってやってみることにした。

まずはCocoaのクラスのSpecをどんなふうに書けばいいのか試してみる。

require 'osx/cocoa'
include OSX

describe NSString do
  before :each do
    @expected_string = 'Cocoa BDD with RSpec'
    @str = NSString.stringWithString(@expected_string)
  end

  it 'should be allocated from String' do
    @str.should be_kind_of(NSString)
    @str.to_s.should be_eql(@expected_string)
  end

  it 'should not be editable' do
    @str.to_s.should be_eql(@expected_string)
    lambda { @str << 'append' }.should raise_error(OCException)
  end

  it 'should be editable' do # わざと間違えてみる
   @str << 'append'
   @str.to_s.should be_eql("#{@expected_string}append")
  end
end

実行結果

$ spec nsstring_spec.rb
..F

1)
OSX::OCException in 'OSX::NSString should be editable'
NSInvalidArgumentException - Attempt to mutate immutable object with setString:
/Library/Frameworks/RubyCocoa.framework/Versions/A/Resources/ruby/osx/objc/oc_attachments.rb:57:in `setString'
/Library/Frameworks/RubyCocoa.framework/Versions/A/Resources/ruby/osx/objc/oc_attachments.rb:57:in `method_missing'
./nsstring_spec.rb:20:

Finished in 0.013094 seconds

3 examples, 1 failure

普通だ。なんらの違和感もない。これはまごうことなきRubyだ。素敵素敵。

あとは自作のクラスを検証するやりかたと、Cocoaアプリの開発サイクルにどう組込むかだねー。あとでやります。