iOSでの単体テストをとてもシンプルにできる！XCTestとOHHTTPStubsを使ってAFNetworkingの単体テストをやってみた


## 概要
[前回](http://qiita.com/asakahara/items/5b5a994782db856741e0)の続きで今回は[OHHTTPStubs](https://github.com/AliSoftware/OHHTTPStubs)を使用して単体テストを行ってみました。
(テスト対象となるソースコードの概要は[前回のコンテンツ](http://qiita.com/asakahara/items/5b5a994782db856741e0)を参照してください)

`OHHTTPStubs`はサーバサイドの処理を用意することなく、クライアントのみでHTTP通信のスタブを用意できるとても便利なライブラリです。


今回は[TRVSMonitor](https://github.com/travisjeffery/TRVSMonitor)というライブラリも一緒に使用しています。
XCTestは非同期処理を待たずにテストケースを終了してしまう場合があるのですが、それを防いでくれるのが`TRVSMonitor`です。


## 手順

`OHHTTPStubs`もCocoaPodsでインストールします。

```
platform :ios, '7.0'
pod "AFNetworking", "~> 2.0"
pod 'KissXML'
target :BlogProjectTest, :exclusive => true do
  pod 'OHHTTPStubs'
  pod 'TRVSMonitor'
end
```

テスト開始時のテストデータを用意する目的で`OHHTTPStubs`を使用します。
今回は使いやすいように`OHHTTPStubs`のユーティリティクラスを用意しました。
必要となるのはレスポンスの結果、ステータスコード、コンテントタイプ、最後にエンドポイントです。
これらを自由にカスタマイズできるのでHTTP通信のほとんどをテストできます。
また今回は使用しませんが、リクエストタイム、レスポンスタイムも調整できます。

```objc
@interface ASHTTPStubUtil : NSObject

+ (void)setupResponseWithFile:(NSString *)fileName statusCode:(int)statusCode
         contentTypes:(NSDictionary *)contentTypes endPoint:(NSString *)endPoint;

+ (void)setupResponseWithData:(NSData *)data statusCode:(int)statusCode
                 contentTypes:(NSDictionary *)contentTypes endPoint:(NSString *)endPoint;

+ (void)setupResponseXML:(NSString *)fileName statusCode:(int)statusCode
                endPoint:(NSString *)endPoint;

+ (void)removeAll;

@end

@implementation ASHTTPStubUtil

+ (void)setupResponseXML:(NSString *)fileName statusCode:(int)statusCode endPoint:(NSString *)endPoint
{
    [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) {
        return [request.URL.absoluteString isEqualToString:endPoint];
    } withStubResponse:^OHHTTPStubsResponse *(NSURLRequest *request) {
        return [OHHTTPStubsResponse responseWithFileAtPath:OHPathForFileInBundle(fileName, nil)
                                                statusCode:statusCode headers:@{@"Content-Type":@"application/rss+xml"}];
    }];
}

+ (void)setupResponseWithFile:(NSString *)fileName statusCode:(int)statusCode
             contentTypes:(NSDictionary *)contentTypes endPoint:(NSString *)endPoint
{
    [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) {
        return [request.URL.absoluteString isEqualToString:endPoint];
    } withStubResponse:^OHHTTPStubsResponse *(NSURLRequest *request) {
        return [OHHTTPStubsResponse responseWithFileAtPath:OHPathForFileInBundle(fileName, nil)
                                                statusCode:statusCode headers:contentTypes];
    }];
}

+ (void)setupResponseWithData:(NSData *)data statusCode:(int)statusCode
             contentTypes:(NSDictionary *)contentTypes endPoint:(NSString *)endPoint
{
    [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) {
        return [request.URL.absoluteString isEqualToString:endPoint];
    } withStubResponse:^OHHTTPStubsResponse *(NSURLRequest *request) {
        return [OHHTTPStubsResponse responseWithData:data statusCode:statusCode headers:contentTypes];
    }];
}

+ (void)removeAll
{
    [OHHTTPStubs removeAllStubs];
}

@end

```

次は実際のテストコードです。
詳細な説明は省きますが、コメントを見てもらえば何をしているかは理解できると思います。
ポイントとしては`TRVSMonitor`を使ってブロック内の処理が完了するまで待機し、処理が完了したら処理を再開してテストを実行しています。

```objc
@implementation ASBlogManagerTests

- (void)setUp
{
    [super setUp];
    // Put setup code here; it will be run once, before the first test case.
}

- (void)tearDown
{
    // Put teardown code here; it will be run once, after the last test case.
    [super tearDown];
}

- (void)testFetchBlogsWithCompletionSuccess
{
    // テストデータの設定
    [ASHTTPStubUtil setupResponseXML:@"BlogList.txt"
                          statusCode:200 endPoint:@"http://blog.hatena.ne.jp/-/hotentry/rss"];
    
    ASBlogManager *manager = [ASBlogManager sharedManager];
    
    // Monitorの生成
    TRVSMonitor *monitor = [TRVSMonitor monitor];
    
    __block NSArray *testResult = nil;
    
    [manager fetchBlogsWithCompletion:^(NSArray *results, NSError *error) {
        
        testResult = results;
        
        // ブロックの処理が完了したらシグナルを送る
        [monitor signal];
    }];
    
    // シグナルを受け取るまで待機する
    [monitor wait];
    
    // テスト実行
    XCTAssertTrue(testResult.count == 2);
    
    ASBlog *blog1 = testResult[0];
    XCTAssertTrue([blog1.title isEqualToString:@"タイトル1"]);
    XCTAssertTrue([blog1.body isEqualToString:@"本文test1"]);
    
    ASBlog *blog2 = testResult[1];
    XCTAssertTrue([blog2.title isEqualToString:@"タイトル2"]);
    XCTAssertTrue([blog2.body isEqualToString:@"本文test2"]);
    
    // テストデータの廃棄
    [ASHTTPStubUtil removeAll];
}

- (void)testFetchBlogsWithCompletionFailNotFound
{
    // テストデータの設定(ステータスコードを404にする)
    [ASHTTPStubUtil setupResponseXML:@"BlogList.txt"
                          statusCode:404 endPoint:@"http://blog.hatena.ne.jp/-/hotentry/rss"];
    
    ASBlogManager *manager = [ASBlogManager sharedManager];
    
    // Monitorの生成
    TRVSMonitor *monitor = [TRVSMonitor monitor];
    
    __block NSArray *testResult = nil;
    __block NSError *testError = nil;
    
    [manager fetchBlogsWithCompletion:^(NSArray *results, NSError *error) {
        
        testResult = results;
        testError = error;
        
        // ブロックの処理が完了したらシグナルを送る
        [monitor signal];
    }];
    
    // シグナルを受け取るまで待機する
    [monitor wait];
    
    // テスト実行
    // エラーのためデータが返ってこないことを期待する
    XCTAssertNil(testResult);
    // エラー情報が含まれることを期待する
    XCTAssertNotNil(testError);
    
    // テストデータの廃棄
    [ASHTTPStubUtil removeAll];
}

@end
```

## 概要
テストデータの切替が容易なので、前回使用した`OCMock`よりもかなりシンプルなテストコードになりました。
HTTP通信周りの単体テストは`OHHTTPStubs`を利用することでかなり楽に行える事が分かりました。
XCTestで単体テストを行う際に少しでも参考になれば幸いです。