As Sloth As Possible

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

タグ:OCUnit

お掃除

iPhoneアプリのコードのお掃除をしている。やれiPadだ新型iPhoneだOS4.0だ、うはぁ頼みの汎用ライブラリはUndocumentedなAPI使ってて動かない、なんですってー他の言語からのトランスレータは規約で禁止だってー、とまぁなんだかんだでiPhoneデベロッパはアプリの改修に追われててAppleに恨みつらみが募ってたりもするんじゃないかと思うけど、まぁ俺も大体そんな感じです。正確に言うとそうじゃなくてもしょっちゅう直してますけど。

リファクタリング

そんなわけで内部のコードの整理とかバグ取りとかついでに切り出したAtomPubクライアント汎用フレームワーク化しようかなーとかやってて、そうなるとちゃんとしたテスト書いてないと辛いのでOCUnitでテストケースをもりもり増やしてるんだけど、非同期にAPIと通信してるところがいまいちテストし辛い。というわけでNSURLConnectionを使ってるところを上手いことテストできないか試行錯誤してみた。

スタブに差し替えておく

まず実際にリクエストを投げないように、NSURLConnectionの実装を差し換える。実装の差し替えって言ったらposeAsClass:あたりかなーとか思ってたら、もうとっくにdeprecatedになってたのね。っていうかそういやそんな話聞いたの大分前だな。

そうするとRuntimeAPIのmethod_exchangeImplementationsあたりでメソッドを差し替えるのがいいのかなー。こんな感じか。

#import <objc/runtime.h>
#import <Foundation/Foundation.h>

void swizzle(Class target,SEL orig_sel,SEL alt_sel) {
    Method orig_method = class_getInstanceMethod(target,orig_sel);
    Method alt_method = class_getInstanceMethod(target,alt_sel);
    method_exchangeImplemantations(orig_method,alt_method);
}

これで例えば、

@interface NSURLConnection (ForTest)
- (void)_stab_start;
@end
@implementation NSURLConnection (ForTest)
- (void)_stab_start {
    // 元の実装だとこれを呼ぶと通信が始まるので、
    // 通信しないで直に
    // [self.delegate connectionDidFinishLoading:self]
    // とか
    // [self.delegate connection:self didFailWithError:error]
    // とかを送る処理を書く。
    // ただ非同期に処理が走(ってるように見せ)る必要があるので、
    // NSTimerとかを使って時間差で呼ぶようにしておく。
}
@end

みたいにしておいて、テストケースの方で

- (void)setUp {
    swizzle([NSURLConnection class], @selector(start), @selector(_stab_start));
}
- (void)tearDown {
    swizzle([NSURLConnection class], @selector(start), @selector(_stab_start));
}
- (void)testConnection {
    id hoge = [[Hoge allco] init]; // 内部でNSURLConnectionを使ってるクラス
    // 通信してますよー的な処理
    [[NARunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:5]];
}

とかやればいいはず。load時じゃなくてsetUpで差し替えてtearDonwで戻してるのは、他のテストにまで影響が出ないように。これでいいんだっけ。差し替えられたら以後そのセレクタになるはずだから戻すときは同じのをもう一度呼べばいいんだよね、多分。

○○はループ説(ひぐらし的な意味で)

ちなみに、テストケース中にそのまま非同期通信を含む処理を書いても上手く動かない。NSTimerとかNSThreadとかを使う処理はNSRunLoopが回ってるときじゃないと期待する動きをしない(例えばNSTimerは発火しないのでタイマーから呼ばれるはずの処理はそもそも通らない)んだけど、テストケースの実行はアプリケーションじゃないのでもちろんNSRunLoopは回ってない。NSURLConnectionの実装を差し替えたおかげで処理にどのくらいの時間がかかるか予想できるので、適当な時間を指定してrunUntilDate:とかで走らせてやればちゃんと動く。

あとは普通にテストケース増やしてけばビルドする度に網羅的にテストが走るので大分安心感が増す。しかしUIの方はどうしたもんかね。こればっかりは見ながらデバッグするしかないのかなー。世のデベロッパのみなさんはどうしてるんだろう。

なんでもターミナルで完結させたい病

最近Objective-Cをお仕事で書いてたりするんだけど、試行錯誤しながらじゃなくてある程度量書くようになってくるとXcodeじゃかったる過ぎるので、ObjC対応済みのctagsをインストールしたりxcodebuildをよしなに叩いてくれるRakefile書いたりしてvimで作業する環境を整えた。もちろんiPhoneシミュレータを起動したりリファレンス読んだりするのでXcodeは開きっぱなしなんだけど、それでも書くのをvimでやれるようになったら大分楽になった。

書くのは楽になったけど、そうなると今度は動作の検証もいちいちシミュレータ起動してあれこれ操作してってのが馬鹿馬鹿しくなってくる。そもそもテストも書かないでアプリ開発とか泣けてくる。幸いiPhoneSDKでもOCUnitが使えるので、ライブラリとして切り出せる部分を別プロジェクトに切り出した上でユニットテストを書くことにする。OCUnitがUnitTest Bundleっていうテンプレートを用意してくれてるので、プロジェクトにUnitTestのターゲットを追加してそれをビルドするだけでテストが走るようになっている。これならrakeからも簡単に実行できていい。

読めないよ

が。OCUnitでのテストをやったことある人は知ってると思うけど、OCUnitの実行結果はそんなに見易くない。

ocunitbuild

読めるかっつー話。もちろんこれはXcode上でも表示されるので、何の装飾もないのも当然。UnitTest Bundleのターゲットをビルドしたときに走るカスタムスクリプトを自分でごにょごにょすればいいっていえばいいんだけど、同じXcodeのプロジェクトを他の人がいじったりすることも有り得るのであんまりそっちで特殊なことをしたくない。Xcode上でビルドしたときはこれはこれでいいのだし。かといってそのままならビルドしてテストまで走らせてくれるようになってるのに、自前でビルドしてパッケージしてテストして表示するRakeタスクを作るのもなんだかなあ。それ両方メンテするの途中で面倒になって結局どっちか放置しそう。

見辛いなら見易くしてやればいいのです

というわけで、xcodebuildを実行した結果を綺麗に整形して表示するスクリプト書いた。さっきのテストがこんな感じに表示される。

rocu

こういうのでいいんだよ、こういうので。緑色になってるとテンション上がるだろ。

ちなみに、STAssert系、NSAssert系のアサーションや、実行時のエラーなどもちゃんと拾う(拾ってくれなきゃ意味ない)。failさせるとこんな感じになる。

rocu_fail

STAssertでこける、つまりテストケースで期待した結果が出てないときは赤字でアサーションのメッセージを出す。アプリ内のアサーションでひっかかってるときは黄色。それ以前にランタイムエラーで実行中に落ちる場合は赤の太字。ちなみに別に自前でテストツール作ったわけじゃないので(あくまでxcodebuild実行時の出力をフィルタしてるだけ)、ランタイムエラーがどこで出てるかまではわからない。そういうのはXcodeのデバッガとかでやってくだしあ。でもNSAssert系のマクロを使うとこけた場所も取れるので、ある程度厳密にやっておきたい値の検証とか積極的にNSAssert使うといいと思う。OCUnitの実行時にはNSLogでprint debugってわけにも行かないしねぇ。

一応コマンドにしてgistに上げてみた。色付き表示にするにはTerm::ANSIColorがいるので注意。色がいらなければ無くてもいい。使い方はソース読むといいと思います。元々Rakefileの中に書いてたのを抜き出してきてやっつけでコマンドにしたやつなので、全然動作の検証とかしてない。なんかツール郡にまとめようかと思ったけど満足しちゃったので。

↑このページのトップヘ