Objective-CでTDDをやってみよう
Objective-CでTDDってどうやるんだっけ?ってなったのでTwitterからPublic Timelineを取得するって流れを簡単にやってみた。
TwitterのAPI利用はTweetingを参考にした。
# ということでiOS5で。
前提
- public timelineを取得するだけ
- 簡単なテストをしたいので
- Xcode4.2を使う
- TwitterやARCなどiOS5の機能を使うので
public timelineを取得するためのインタフェースを考える
- TTTwitterというクラスにする
- sendPublicTimelineToDelegate:withSelector:というメソッドを使うとパラメータで渡したdelegateのselectorに結果のtimelineを渡してくれる
- 結果の形式はstatusとbodyというkeyをもつdictionary
- statusにはHTTPレスポンスのstatusCodeが入っている
- bodyには"twitter timelineのjsonの結果"が入っている
事前準備
- 新規プロジェクトを作る画面に行き、Single View Applicationを選んでひな形を作成
- Project NameはTDDTwitterとする
- サイドバーのFrameworksのグループを展開し、UIKit.frameworkを右クリックしてfinderで開く
- Finderの中にTwitter.frameworkがあるので、ドラッグ&ドロップでFrameworksグループのUIKit.frameworkの下辺りに入れる
- このときCopyはしない。デフォルトのままFinish押せばいい
まずはロジックテストを作る
- 「File > New > New Target」を選ぶ
- もしくはプロジェクトを選んだ画面の左下にある「Add Target」をクリック
- Otherで「Cocoa Touch Unit Testing Bundle」を選んでNext
- ロジックテストなので名前を「TDDTwitterLogicTests」としてFinish
とりあえずロジックテストを走らせる
クラスとメソッドを作る
さてさて、public timelineを取得するクラスとメソッドを作ろう。
作りたいのは
- メソッドを実行するとNSDictionaryが返ってくる
というもの。
だけども、TWRequest:performRequestWithHandler:はblockを受け取って非同期で処理をするため、
- メソッドにdelegateとselectorを渡すと
- selectorが呼ばれて、その引数にNSDictionaryが付いている
という感じにしました。
なのでTest側は
TTTwitter *twitter = [[TTTwitter alloc] init]; [twitter sendPublicTimelineToDelegate:self withSelector:@selector(stopLoopAndSetPublicTimeline:)];
という感じで呼び出して、stopLoopAndSetPublicTimelineメソッドで結果を保持してtestします。
ということでTTTwitter.hとTTTwitter.mを作る必要がありますね。
TTTwitterを作る
- TDDTwitterグループを右クリックして「New File」を選ぶ
- Cocoa Touchの「Objective-C class」を選んでNext
- ClassをTTTwitterとしてNext
- Targetsの「TDDTwitterLogicTests」にもチェックを入れて「Create」
TTTwitter.hはこんな感じでメソッドのみ。
// TTTwitter.h #import <Foundation/Foundation.h> #import <Twitter/Twitter.h> @interface TTTwitter : NSObject - (void)sendPublicTimelineToDelegate:(id)aDelegate withSelector:(SEL)aSelector; @end
TTTwitter.mはその実装(Tweetingほぼそのまま)
#import "TTTwitter.h" @implementation TTTwitter - (void)sendPublicTimelineToDelegate:(id)aDelegate withSelector:(SEL)aSelector { TWRequest *postRequest = [[TWRequest alloc] initWithURL:[NSURL URLWithString:@"http://api.twitter.com/1/statuses/public_timeline.json"] parameters:nil requestMethod:TWRequestMethodGET]; // Perform the request created above and create a handler block to handle the response. [postRequest performRequestWithHandler:^(NSData *responseData, NSHTTPURLResponse *urlResponse, NSError *error) { NSError *jsonParsingError = nil; NSArray *key = [NSArray arrayWithObjects:@"status", @"body", nil]; NSArray *val = [NSArray arrayWithObjects:[NSNumber numberWithInteger:[urlResponse statusCode]], [NSJSONSerialization JSONObjectWithData:responseData options:0 error:&jsonParsingError], nil]; NSDictionary *publicTimeline = [NSDictionary dictionaryWithObjects:val forKeys:key]; if ([aDelegate respondsToSelector:aSelector]) { // aDelegateはidなので警告が出るのは仕方ないのかなぁ。何かやりようがあるかなぁ。 [aDelegate performSelector:aSelector withObject:publicTimeline]; } else { NSLog(@"...... No selector ......"); } }]; } @end
テスト側も書く
テスト側も書かないといけませんねということで、さっきの続き
TTTwitter *twitter = [[TTTwitter alloc] init]; [twitter sendPublicTimelineToDelegate:self withSelector:@selector(stopLoopAndSetPublicTimeline:)]; // main thread は sub treadが_isDoneをYESにするまでloop while (!_isDone) { NSLog(@"Polling..."); }
TWRequestが非同期処理をして、main threadに処理が返ってくるので
インスタンス変数_isDoneがYESになるまで(非同期処理が終わるまで)ループ。
非同期処理が終わるとselectorが呼ばれるので自分のpropertyにsetするメソッドを作っておく。
- (void)stopLoopAndSetPublicTimeline:(NSDictionary *)aPublicTimeline { // sub threadで実行される _isDone = YES; // main threadでないとtestに失敗してもプログラムが正常終了してしまうので // ここではpropertyのsetだけして抜ける self.publicTimeline = aPublicTimeline; }
そうするとwhileから抜けるので、whileの後にpropertyの値をtestする。
STAssertNotNil(self.publicTimeline, @"public timelineはnilではない"); NSNumber *statusCode = [self.publicTimeline objectForKey:@"status"]; STAssertNotNil(statusCode, @"statusというkeyをもっている"); id body = [self.publicTimeline objectForKey:@"body"]; STAssertNotNil(body, @"bodyというkeyをもっている"); if ([statusCode intValue] == 200) { STAssertTrue([body isKindOfClass:[NSArray class]], @"200のときはbodyの値はNSArrayのインスタンス"); } else if ([statusCode intValue] == 400) { STAssertTrue([body isKindOfClass:[NSDictionary class]], @"400のときはbodyの値はNSDictonaryのインスタンス"); } else { NSLog(@"statusCode: %@", statusCode); }
いきなり非同期処理とかでハマったけど大体どうやるかわかったのでよしとする。
https://github.com/monmon/TDDTwitter