お掃除

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の方はどうしたもんかね。こればっかりは見ながらデバッグするしかないのかなー。世のデベロッパのみなさんはどうしてるんだろう。