今度はXcode上でアプリケーションを作るときに使えるかやってみる。

アプリケーションで使うクラスを全部フレームワークとしてビルドして、RubyからOSX#requiew_frameworkすれば自作のObjective-CクラスをRSpecにかけられることは前回まででわかったのだけど、流石にそれはダルい。プロジェクト内にmyclass_spec.rbってファイルを置いといてSpecターゲットをビルドするとそれらを自動で検証してくれたりするのが理想なわけです。

そんなわけで、こういう手順でやってみた。

  1. 普通にCocoa Applicationのプロジェクトを作る
  2. プロジェクトにRubyCocoa Applicationを作るビルドターゲットを追加する
    1. RubyCocoa Applicationを起動すると、'_spec.rb'のファイルを読み込んでSpec Runnerで実行するようにする
  3. xxx_spec.rbを書く
  4. xxx.m (Objective-Cのクラス)を実装する
  5. RubyCocoaのビルドターゲットの方で「ビルドして実行」

ビルドターゲットを最初Shell Toolで作ろうとしたんだけど、それだとリソースとしてRubyスクリプトを含められないので外部から読み込んだりといろいろ面倒くさい。その辺はまた後で考えるとして、とりあえずRubyCocoa Applicationにしてmain内でアプリケーションループを開始しないでスクリプトだけ実行して終了するようにした。これならクラスやスクリプトは自動で読み込まれるしGUIは表示されない。

Specターゲットの方のmain.mとrb_main.rbはこんな感じ。

#import <Cocoa/Cocoa.h>
#import <RubyCocoa/RBRuntime.h>

int main(int argc, const char *argv[])
{
    return RBApplicationMain("rb_main.rb", argc, argv);
}
require 'osx/cocoa'
require 'rubygems'
require 'spec'

def rb_main_init
  path = OSX::NSBundle.mainBundle.resourcePath.fileSystemRepresentation
  specs = []
  rbfiles = Dir.entries(path).select {|x| /?.rb?z/ =? x}
  rbfiles -= [ File.basename(__FILE__) ]
  rbfiles.each do |path|
    if path =? /_spec/
      specs << File.join(File.dirname(__FILE__), File.basename(path))
    else
      require( File.basename(path) )
    end
    ::Spec::Runner::CommandLine.run(specs, STDERR, STDOUT, true, true) unless specs.empty?
  end
end
if $0 == __FILE__ then
  rb_main_init
  #OSX.NSApplicationMain(0, nil)
end

これでResource内のxxx_spec.rbを全部読み込んでSpecRunnerにかけてくれる。試しに実装してみる。

Namakemono.h

#import <Cocoa/Cocoa.h>

@interface Namakemono : NSObject
{
	NSString * name;
}
- (id)initWithName:(NSString *)aName;
- (NSString *)name;
- (void)setName:(NSString *)aName;
- (NSString *)greetingMessage;
@end

namakemono_spec.rb

include OSX

describe Namakemono do
  before :each do
    @faul = Namakemono.alloc.initWithName(NSString.stringWithString('faultier'))
  end
  
  it 'should be Namakemono' do
    @faul.should be_kind_of(Namakemono)
  end
  
  it 'should have name' do
    @faul.name.to_s.should eql('faultier')
    @faul.greetingMessage.to_s.should match(/Hello, my name is #{@faul.name}/)
  end
end

「ビルドして実行」

[Session started at 2007-07-23 02:07:18 +0900.]
..

Finished in 0.014898 seconds

2 examples, 0 failures

Spec はステータス 0 で終了しました。

ばっちりだー!そうそう。そういうの、俺が望んでたのは。視覚的に面白いかどうかはともかく(C#のNUnitみたいなGUIツール作るのがいいかなぁ)、SpecをRubyで書けるようになったことで飛躍的に可読性と柔軟性が上がる。あと、個人的な趣味と会社でも使ってて慣れてるからRSpecにしたけど、別にTest::Unitを使ってもいいよな。ていうかRuby test classってテンプレートあるんだけど、もしかしてもう俺がやろうとしてるような機構がRubyCocoaに組込まれてるんだろうか。…なんか段々車輪の再発明じゃないかと不安になってきた。faulって馬鹿だなーと思った方は是非つっこんで下さい。

まぁでも、マインドセットとしてはTDDよりBDDの方が良いと思うし、テスティングフレームワークであるTest::Unitより仕様記述用のDSLっぽく実装されてるRSpecの方がより可読性は高いと思うし、だから敢えて使うんだ、と、いうことで。と、いうことで。

さてここまで来たわけだけども、歓喜の踊りを踊るのはにはまだ早い。毎回無意味にビルドターゲットにRubyCocoa Applicationを入れて一から実装するのはかなり嫌なので、Specを読み込んで実行するツールだけ別に作っておいて、RSpecのスクリプトとObjective-Cのクラスを含んだローダブルバンドルをビルドターゲットにして、そいつをビルドフェーズの最後でツールに読み込ませて実行するようにしたい。できればunit test bundleみたいにテンプレにしてしまえればいいんだけど。Xcodeのテンプレって自分でも作れるんだよね、多分。知らないけど。

うーん。眠くなってきたので今日はここまで。