今度はXcode上でアプリケーションを作るときに使えるかやってみる。
アプリケーションで使うクラスを全部フレームワークとしてビルドして、RubyからOSX#requiew_frameworkすれば自作のObjective-CクラスをRSpecにかけられることは前回まででわかったのだけど、流石にそれはダルい。プロジェクト内にmyclass_spec.rbってファイルを置いといてSpecターゲットをビルドするとそれらを自動で検証してくれたりするのが理想なわけです。
そんなわけで、こういう手順でやってみた。
- 普通にCocoa Applicationのプロジェクトを作る
- プロジェクトにRubyCocoa Applicationを作るビルドターゲットを追加する
- RubyCocoa Applicationを起動すると、'_spec.rb'のファイルを読み込んでSpec Runnerで実行するようにする
- xxx_spec.rbを書く
- xxx.m (Objective-Cのクラス)を実装する
- 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のテンプレって自分でも作れるんだよね、多分。知らないけど。
うーん。眠くなってきたので今日はここまで。