As Sloth As Possible

可能な限りナマケモノでありたい

タグ:RSpec

sinatraとActiveRecordとERBでBBS作ったのでソースを公開してみる - だるろぐに触発されて俺もBBSを作ってみようと、ここ数日Sinatraをいじっていた。Sinatraさんは最近バージョンアップしてた気がするけど、どうやらちゃんとRuby1.9.1でも動くようだ。素敵。

で、順調に行くかと思ったんだけど、どうにも書き込みのspecが通らない。そこで初めて、POSTやGETでパラメータにマルチバイトの文字列が入ってると何かおかしいことに気付いた。

最初に書いたspecとアプリ側のコードを抜粋。

# coding: utf-8
require 'rubygems'
require 'rack/test'
require 'routes.rb' #sinatraアプリ

set :environment, :test

include Rack::Test::Methods

def app 
  Sinatra::Application
end

describe '投稿するとき' do
  it 'は投稿に成功したら一覧にリダイレクトすること' do
    @props = { 
      'user'  => 'セト',
      'body'  => 'べ、別にアンタの論文が心配なんじゃないんだから!',
    }
    Entry.should_receive(:create).with(@props)
    post '/add', @props
    last_response.status.should be_equal(302)
  end 
end
# coding: utf-8
require 'rubygems'
require 'sinatra'
require 'haml'
require 'sequel'

post '/add' do
  Entry.create(params)
  redirect '/'
end

DB = Sequel.connect('sqlite://db/trabbs.db')
class Entry < Sequel::Model; end

実際にはget '/'とかEntryのスキーマとかindex.hamlとかも用意してありますがそれは心の目で補完しておくれ。んで、期待してるのはこれでセトがツンデレな投稿をしてそれがDBに記録され、その後トップにリダイレクトされることなんだけど、これがコケる。

1)
Spec::Mocks::MockExpectationError in '投稿するとき は、投稿に成功したら一覧にリダイレクトすること'
 expected :create with ({"user"=>"セト", "body"=>"べ、別にアンタの論文が心配なんじゃないんだから!"}) but received it with ({"user"=>"\xE3\x82", "body"=>"\xE3\x81\xB9\xE3\x80\x81\xE5\x88\xA5\xE3\x81\xAB\xE3\x82\xA2\xE3\x83\xB3\xE3\x82\xBF\xE3\x81\xAE"})

ありゃ。UTF-8の文字列がくるべきところに、ASCII-8BIT、つまりバイト列がそのまま来ちゃってる。んむ、えーと、実際のバイト列を変更せずにエンコーディングだけ変えるのはforce_encodingだったよね。ということでforce_encoding('UTF-8')してみる。

1)
Spec::Mocks::MockExpectationError in '投稿するとき は、投稿に成功したら一覧にリダイレクトすること'
 expected :create with ({"user"=>"セト", "body"=>"べ、別にアンタの論文が心配なんじゃないんだから!"}) but received it with ({"user"=>"\xE3\x82", "body"=>"べ、別にアンタの"})

あ、あれ?なんか足りなくない?これはどういうこと?

Rack::Utils#escape

ところで、上では端折ったけど、実はspecを実行する際にこんなwarningが出ている。

/usr/local/lib/ruby/gems/1.9.1/gems/rack-1.0.0/lib/rack/utils.rb:13: warning: regexp match /.../n against to UTF-8 string

UTF-8に対してエンコーディングを無視して正規表現にマッチさせてる…うん、どうもこれが怪しい。さてこれはどこから出てるかというと、こんなコードから出ている。

# rack-1.0.0/lib/rack/utils.rb
module Rack
  # Rack::Utils contains a grab-bag of useful methods for writing web

  module Utils
    # Performs URI escaping so that you can construct proper
    # query strings faster.  Use this rather than the cgi.rb
    # version since it's faster.  (Stolen from Camping).
    def escape(s)
      s.to_s.gsub(/([^ a-zA-Z0-9_.-]+)/n) {
        '%'+$1.unpack('H2'*$1.size).join('%').upcase
      }.tr(' ', '+')
    end
    module_function :escape

    # Unescapes a URI escaped string. (Stolen from Camping).
    def unescape(s)
      s.tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/n){
        [$1.delete('%')].pack('H*')
      }
    end
    module_function :unescape

    # 以下省略
  end
end

なるほどですねー。要するに、

  1. escape中でunpackするときに、String#sizeを使っている。1.8まではこのメソッドはバイト数を返してたけど、1.9では文字数を返すように仕様が変更されている。マルチバイトな文字列だと当然バイト数と文字数は一致しないので、途中で切れちゃうことになる。
  2. packして返ってくるのは単なるバイト列、つまりASCII-8BITのString。
  3. 1.8までは文字列がどんなエンコーディングであれ、unpackしてpackすればまた文字列に戻る。でも1.9ではunpackからpackの過程で元のエンコーディング情報は失なわれているので、明示的にエンコーディング情報を付加してやる必要がある

ということかしら。まず1に関してはString#sizeを使ってるところをString#bytesizeを使うように書き換えればいい。ちなみにRack::Utilsでは1.8でも1.9でも動くようにRack::Utils#bytesizeというメソッドが定義されているので、それを使うといい。というか、なんで使ってないんだろう。単に忘れてるんだろうなぁ…。

module Rack
  module Utils                                                                          
    def escape(s)
      s.to_s.gsub(/([^ a-zA-Z0-9_.-]+)/) {                                              
        '%'+$1.unpack('H2'*bytesize($1)).join('%').upcase                               
      }.tr(' ', '+')
    end
  end
end

これをrackをrequireした後に書いてRack::Utils#escapeを上書きしてやると、ちゃんとescapeされるようになる。

次はASCII-8BITになっちゃってるのをforce_encodingしてエンコーディングを変える。これも本当はRack::Utils#unescapeを上書きして、その中でエンコーディングを推測してforce_encodingしておいて、アプリ側のフックでString#encodingを使って内部で使うエンコーディングに変換してやるのがいいかなぁとか思ったんだけど、それでいいのかなー。

んでもまぁ、今回はUAに応じてviewのエンコーディング変えるみたいなことはしてないし、UTF-8決め打ちで良いよね☆…というわけで、Sinatraのbeforeの中で、RUBY_VERSIONが1.9以上だったときはparamの文字列をforce_encoding('UTF-8')するようにしたった。良い子はちゃんとして下さい。

はい、これでちゃんとspec通りました。ふー。

…で。

そしたら、今度はRackをforkして直してpull request送ればいいんだよね。いいんだけど、やんなきゃなーとは思うんだけど、めんどいその辺のあんま自信ないので、誰か1.9でもちゃんと動くように直してRackの開発スタッフに教えてあげてもらえると助かります。助けてエロい人。

大分放置してましたけど思い出したかのように続き。いや実際忘れてたのだけども。

とりあえず以前書いたものたち。

spceコマンドのラッパを書く

前回は自分のリソース内の*_spec.rbを読み込んで自分のmainバンドルのクラスをテストする、というRubyCocoaアプリを作ったのだけど、specを走らせるためだけに毎回そんなものを作るのは面倒くさすぎる。実際やってることはSpec::Runner::CommandLineにspecファイルを渡してるだけなので、ローダブルバンドルを引き数に取ってspecを走らせる、というspecコマンドのラッパを用意する。

#!/usr/bin/env ruby

require 'rubygems'
require 'spec'
require 'osx/cocoa'

spec_opts   = ARGV.reject {|opt| opt =~ /\.bundle\z/}
bundle_path = ARGV.find {|opt| opt =~ /\.bundle\z/}

if bundle_path && bundle = OSX::NSBundle.bundleWithPath(File.expand_path(bundle_path))
  bundle.load
  bundle
  Dir.foreach(bundle.resourcePath) {|file| spec_opts.push(File.join(bundle.resourcePath, file)) if file =~ /.+_spec.rb\z/}
end

::Spec::Runner::CommandLine.run(spec_opts, STDERR, STDOUT, true, true)

見ての通り、バンドルをロードして*_spec.rbを抜き出し、それらのパスをコマンドライン引数に付け加えてSpecのランナーに渡すだけ。バンドルは最初の一個以外は全部無視する設定になってるけど、俺が想定してる使用方法で扱うバンドルは一個だけなのでとりあえずこれで十分。必要であれば全部受けつけてもいいけど。

拡張子が.bundleのファイル以外の引数は全てspecのランナーに渡るので、適当な名前付けてパスの通ったとこに配置しておくとspecコマンドの代わりに使える。-fとか-cとか-sとかも問題なく使えたりする。

使用方法

で、こいつを実際にどう使うかというと、例えばXcodeを使っているならこんな感じ。

  1. 新規ターゲット追加でCocoa Loadable Bundleのターゲットを追加
  2. そのターゲットに次のスクリプトを実行するビルドフェーズを追加:ruby <上のスクリプトのパス> [<specオプション>] $BUILD_DIR/$CONFIGURATION/$EXECUTABLE_NAME.bundle
  3. そのターゲットにObjective-Cのクラスとspecファイルを入れる(その際specファイルは必ず末尾に「_spec.rb」を付ける必要がある)

これで、このターゲットをビルドすると自動的にspecを実行してくれるようになるという噂。いや、ちゃんとなりました。なりましたよ。スペックが全て通ると何事もなくビルド完了するけど、スペックが通らないとビルドが失敗しビルド結果画面にspecコマンドの結果が出る。例えばビルドターゲットの依存関係でアプリターゲットがspecターゲットに依存するようにしておくと、specが全て通らない限りアプリがビルドできなくなるのでいい感じ。

注意点いくつか

まず当然のことながらRubyCocoaがインストールされてないと動かない。Leopardだと最初から/System/Library/Frameworks/RubyCocoa.frameworkがあるんだけど、それはもともと入ってるruby(/usr/bin/ruby)からしか使えない。普段から/usr/bin/rubyを使ってる人は別に問題ないけど、それ以外*1を使ってる場合、そのRuby用に再度RubyCocoaを入れるか、/usr/bin/rubyの方でRSpecを入れるかしておく必要がある。

あと、RSpecの最新版だとspecの実行結果をsuccessとfailedの他にpendingにすることができるけど、pendingはsuccessと同じ扱いになってビルド成功してしまう。ので、ご注意を。まぁ、実行結果はビルドログに吐きだされてるのでそれを見ることはできるけども。

その他

とりあえず結果表示をどうにかしたい。Growlに通知投げたりしたら面白いかなぁ。

*1:/usr/local/bin/rubyとか/opt/local/bin/rubyとか

今仕事でRailsアプリケーションを組むときに、test/unitじゃなくてRSpecを使ってる。mock周りの使い勝手がいいとか、語彙が馴染みやすいとかいろいろ魅力があるんだけど、その「可読性」を保つにはなかなかコツがいると思う。

言うまでもなくRSpecはRubyのコードを「英語の表現として自然に見える」ようにすることを意図して語彙や書き方を決めている。これは英語圏のエンジニアには非常に素敵なことではあるんだけど、英語が苦手で英作文なんて始めて数分で泣きたくなるようなへたれ外国語学部生にとっては正直やっかいだし、周りの人達の大半は英語に慣れていない人達*1だったりするので、せっかく可読性が高い綺麗な表記でさえむしろ意図を理解する妨げになったりする。いっそドイツ語で書いて「お勉強」に活用してやろうかという衝動に駆られたけども、誰一人として読めない上に一週間後の俺ですら理解に苦しみそうなのでそんな暴走は妄想の中だけにしておく。

ということで、やっぱりSpecやらコメントやらといったものは、とっつきやすくするためにもできるだけ日本語で書きたいところ。ところが、英語の語順を前提にしているのでちょっと工夫しないとコードとして見たときかspecコマンドの出力かどっちかが不自然になるんだよなぁ。

Lesson1:I study english編

まず、RSpecの記述はどういうふうに書くかというと、

describe ClassName, 'context' do
end

となる。で、その条件下にあるときの挙動について、

it 'should ...' do
end

で列挙していく。例えば、最近各方面で名前を聞くことの多いあの先生の仕様を書くなら、

describe Teacher, 'when introduce himself' do

  it 'should tell own name' do
    momoiro = Teacher.new('糸色 望')
    momoiro.name.should be_eql('糸色望') #桃色係長の名前をつなげて書いちゃだめですよ。
  end

end

こんな感じ。describe〜やit〜もさることながら、仕様確認の処理の記述も'momoiro('s) name should be equal "糸色望".'とちゃんと読めていい感じ。で、これを実行すると、

'Teacher when introduce himself should tell his own name' FAILED
expected "絶 望", got "糸色 望"

という出力をしてくれる。この'Teacher when introduce himself should tell his own name'が綺麗に文章になっていて*2「RSpecは軸がぶれてない 素敵」と口ずさみたくなる感じ。いや、軸関係ないけど。人として。でもこれは英語が'Something when ...'で一つの主語になるから綺麗に'it'に繋がるのであって、日本語で単純に置き換えるとちょっと不自然になる。

Lesson2:仕様なんぞ箇条書きで十分なんです編

最初はもういっそ英語的な部分は無視して、

  • 先生が自己紹介をするケース
    • 自分の名前を言う
    • 横書きにされると絶望する
    • 命名権には数十円から数百円/日必要

という箇条書きのイメージで、contextには条件を、'should ...'の部分には説明を書いてしまえと

describe Teacher, '先生が自己紹介をするケース' do
  it '自分の名前を言う' do
    ...
  end
  it '横書にされると絶望する' do
    ...
  end
end

と書いてみたところ、

'Teacher 先生が自己紹介をするケース 自分の名前を言う' FAILED

となってしまった。ぬうん。なんか、文章にならないのが気持ち悪い。最初のTeacherもちょっと気持ち悪い。どうせなら、出力も日本語として違和感無く読める物@ITの記事より)であるといいなあと思うんだよなぁ。

Lesson3:specコマンドは日本語で喋り始めました編

じゃあ、ということで、さっきの画像にならって、出力の文章が繋がるようにしてみる。

describe Teacher, '先生が自己紹介をするという状況では' do
  it '先生は自分の名前を言うんだ' do
    ...
  end
  it '先生は横書にされると絶望するんだ' do
    ...
  end
end
'Teacher 先生が自己紹介をするという状況では 先生は自分の名前を言うんだ' FAILED

…出力はいいんだけど、肝心のSpecの記述、日本語が浮いちゃっててなんだかな。Teacherやitが完全に無視されてるのも可哀想スマートでない。

Lesson4:俺は妥協しないぜ!編

で、大腸のしくみについてよく考えた*3結果、「が〜〜するとき」と「は〜〜すること」というふうにすると不自然さが若干緩和されるなぁというところに至る。

describe Teacher, 'が自己紹介をするとき' do
  it 'は、自分の名前を言うこと' do
    ...
  end
  it 'は、名前を横書きにされると絶望すること' do
    ...
  end
end
'Teacher が自己紹介をするとき は、自分の名前を言うこと' FAILED

やった。これならSpecの方では「それ(it)は、〜〜なこと」と読めるし、出力は「〜〜するときは、〜〜なこと」と読めてどちらも自然。俺ってば軸がぶれてない、素敵。…結局それが言いたいだけか。

*1:以前ある人がRailsの書籍の情報が若干古くてハマってたので、「ググって調べて」って言ったらさらに古いバージョンの日本語APIリファレンス読んでて、なんで一次資料見ないのって突っ込んだら「英語だから…」って言われたときは泣けた。気持ちはわかるけど、勘弁してよ…。

*2:「つーか英文間違ってるじゃねーかこのゆとり世代が!」って思ったら激しく指摘してください。「っるっせーわこのダラズが、そんぐらい知っとるわい!!ネタに決まってんじゃろがボケが!!」と感謝の言葉を述べながら即座に修正します。

*3:ネタ古いなぁ。なんでこんなの唐突に思い出したんだろう

今度は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のテンプレって自分でも作れるんだよね、多分。知らないけど。

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

BDDでCocoa開発をするのにRSpecを使ってみるの続き。

なんだか意外と簡単に使えそうだったのでこんどはCocoa組込みじゃないフレームワークで試してみよう、ということでOgreKit。こちらは事前にコンパイルして/Library/Frameworks以下に配置済。と、ここでそもそもどうやってフレームワークを呼び出すのかわからなくて一思案。本家サイトでなんか書いてないかなーと探してみるも、どこに書いてあるんだかわからん。ActiveRecordSupportとか興味深いことが書いてあるのも見つけたけどとりあえず後回し。

サンプルのソースを見てたらOSX.require_frameworkを使うらしいというのはわかったんだけど、読み込んだフレームワークのクラスがどこにあるのかわからずしばらく悩んだ。OgreKit::OGRegularExpressionとかやってみてそんなもん無いと怒られたり。冷静に考えれば、Objective-CだったらRubyみたいにモジュールがネストしてないのでOSX::OGRegularExpressionがでよかったんだけど、それを見つけるまでGoogle先生を質問攻めにしてしまった。さてそんなわけでできたのがこんな感じ。

require 'osx/cocoa'
include OSX
require_framework 'OgreKit'

describe OGRegularExpression do
  before :each do
    @match_string   = 'Ich liebe dich, aber du liebst ihn. Ich liebe dich...'
    @unmatch_string = 'Ich mag nicht nur Ruby, sondern Objective-C.'
    @expression     = 'dich'
    @ogregex        = OGRegularExpression.regularExpressionWithString(@expression)
  end
  
  before :all do
    @result         = []
  end
  
  after :all do
    p @result
  end 
  
  it "should replace matches" do
    # Rubyのメソッド呼び出しっぽいスタイル
    replaced = @ogregex.replaceAllMatchesInString_withString(@match_string, 'mich')
    replaced.to_s.should_not include(@expression)
    @result << replaced.to_s 
  end
  
  it 'should replace nothing when no matches in string' do
    # こっちはObjective-Cのメッセージパッシングっぽいスタイル
    not_replaced = @ogregex.objc_send(:replaceAllMatchesInString, @unmatch_string, :withString, 'mich')
    not_replaced.to_s.should eql(@unmatch_string)
    @result << not_replaced.to_s
  end
end
$ spec ogrekit_spec.rb
..["Ich liebe mich, aber du liebst ihn. Ich liebe mich...", "Ich mag nicht nur Ruby, sondern Objective-C."]


Finished in 0.018822 seconds

2 examples, 0 failures

でけたでけた。変換の例に使ってる文章が何なのかは気にしない方向で。OgreKitのSpecなのに正規表現使ってないのも気にしたら負けかなと思ってる。で、OgreKitは既に有効なロードパスの中にあったのでフレームワーク名で呼び出せたけど、OSX#require_frameworkは引数にフレームワークのフルパスも与えられるので、フレームワーク化してしまえば自分で作った任意のObjective-Cクラスも簡単に扱える。まだ試してないけどバンドルをロードする機能もある。思ったんだけど、gemのパッケージを自動生成するやつみたいにwigetのファイル構成とかビルドするRakefileとかのジェネレータを作ったら便利かもしれない。wdigetならインターフェースもバンドルの開発もCUI上で完結できるし、RSpecで確認しながら作るスタイルは向いてるかも。Dashcodeが出ようってご時世に何を言ってるんだか…でもvimのが楽なんだもん、Xcodeでいろいろやるより。

あと、初めてRubyCocoaを見たときにも思ったんだけど、Objective-Cはメソッド名が長いなぁやっぱり。CamelCaseもRubyの中にあるとどうしても違和感が拭えない。慣れるっきゃないかな。Objective-Cで書いてるとそんなに気にならないんだけど。何でだろ。

さて、なんだかRubyCocoaを触ってみよう、なノリになってきたけど、目的はRSpecでもRubyCocoaでもなくて「CocoaでBDD」。これをどうにかしてCocoaアプリの開発に取り入れてみたいと思う。いちいちアプリをフレームワーク化してビルドしてterminalからspec実行して、とかやりはじめたら本末転倒。Xcodeで上手く自動実行させる方法を考えよう、と。

前々から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アプリの開発サイクルにどう組込むかだねー。あとでやります。

↑このページのトップヘ