読者です 読者をやめる 読者になる 読者になる

Objective-CでTDDをやってみよう

Objective-C TDD

Objective-CでTDDってどうやるんだっけ?ってなったのでTwitterからPublic Timelineを取得するって流れを簡単にやってみた。
TwitterAPI利用はTweetingを参考にした。
# ということでiOS5で。


前提

  • public timelineを取得するだけ
    • 簡単なテストをしたいので
  • Xcode4.2を使う
    • TwitterやARCなどiOS5の機能を使うので

public timelineを取得するためのインタフェースを考える

  • TTTwitterというクラスにする
  • sendPublicTimelineToDelegate:withSelector:というメソッドを使うとパラメータで渡したdelegateのselectorに結果のtimelineを渡してくれる
  • 結果の形式はstatusとbodyというkeyをもつdictionary

事前準備

  1. 新規プロジェクトを作る画面に行き、Single View Applicationを選んでひな形を作成
    • Project NameはTDDTwitterとする
  2. サイドバーのFrameworksのグループを展開し、UIKit.frameworkを右クリックしてfinderで開く
  3. Finderの中にTwitter.frameworkがあるので、ドラッグ&ドロップでFrameworksグループのUIKit.frameworkの下辺りに入れる
    • このときCopyはしない。デフォルトのままFinish押せばいい

まずはロジックテストを作る

  1. 「File > New > New Target」を選ぶ
    • もしくはプロジェクトを選んだ画面の左下にある「Add Target」をクリック
  2. Otherで「Cocoa Touch Unit Testing Bundle」を選んでNext
  3. ロジックテストなので名前を「TDDTwitterLogicTests」としてFinish

とりあえずロジックテストを走らせる

  1. ツールバーのスキームメニューを開き、TDDTwitterLogicTestsのスキームをSimulatorにする
  2. Productメニューから「Test」を実行する
    • (または⌘Uをクリック。多分UnitTestのUかな?)
  3. Viewメニューから「Navigators > Issue」を選択
    • (またはサイドメニューの左から4番目の△に!のマークのボタンをクリック)
    • そうするとデフォルトでSTFailが仕掛けてあるのでtestに失敗しているのが分かる
  4. 下のログ部分を確認すると詳細が出力されている
    • Debug Areaが表示されてない場合はViewから表示させるか、shift+cmd+Yのショートカットキーを使う

クラスとメソッドを作る

さてさて、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を作る

  1. TDDTwitterグループを右クリックして「New File」を選ぶ
  2. Cocoa Touchの「Objective-C class」を選んでNext
  3. ClassをTTTwitterとしてNext
  4. 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


まんま参考にしたunit testの説明ドキュメント

Unit Testing Applications日本語訳pdf