XCTestとOCMockを使ってAFNetworking 2.0の単体テストを行う


## 概要
`AFNetworking 2.0`利用時の単体テストを自動化するためにテストコード書いてみた際のログです。
あまりここら辺の知見がないので、もっとこうした方がいいなどのアドバイス等あれば是非お聞かせください。

やった内容としては、はてなブログの人気エントリーを返すRSSフィードをリクエストし、その結果としXMLが返ってくるので、その部分を`OCMock`を使ってXMLを返すようにメソッドの結果を書き換えることで単体テストを実現しています。

・はてなブログの人気エントリー
http://blog.hatena.ne.jp/-/hotentry/rss

## 手順

`OCMock`と`AFNetworking`のインストールにはCocoaPodsを使用するため`Podfile`を作成します。
XMLを扱うので`kissXML`を一緒に入れています。

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

下記コマンドを実行してインストールしてください。

```
pod install
```

次にXMLの内容をBlogList.txtファイルに格納します。
中身はこんな感じです。

```xml
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:media="http://search.yahoo.com/mrss">  <channel>    <title>人気エントリー</title>      <item>        <title>タイトル1</title>        <description>本文test1</description>      </item>          <item>        <title>タイトル2</title>        <description>本文test2</description>      </item>  </channel></rss>
```

`AFNetworking 2.0`を使ってRSSフィードへリクエストを投げる処理はこんな感じで実装しています。
ここでは返却されたXMLをパースして`NSDictionary`に一旦変換してから更に`ASBlog`エンティティにパースしています。

```objc
#import "ASBlogManager.h"
#import "AFHTTPRequestOperationManager.h"
#import "DDXMLDocument.h"
#import "DDXMLElement+Dictionary.h"
#import "ASBlog.h"

@implementation ASBlogManager

+ (instancetype)sharedManager
{
    static ASBlogManager *_sharedManager= nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _sharedManager = [[ASBlogManager alloc] init];
    });
    
    return _sharedManager;
}

- (void)fetchBlogsWithCompletion:(void (^)(NSArray *results, NSError *error))block
{
    AFHTTPRequestOperationManager* manager = [AFHTTPRequestOperationManager manager];
    manager.responseSerializer = [AFHTTPResponseSerializer serializer];
    __weak typeof(self) weakSelf = self;
    
    [manager GET:@"http://blog.hatena.ne.jp/-/hotentry/rss"
      parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
          
          NSDictionary *xml = nil;
          NSError *parseError = nil;
          DDXMLDocument *doc = [[DDXMLDocument alloc] initWithData:responseObject options:0 error:&parseError];
              
          if (!parseError) {
              // 取得したXMLの内容をNSDictionaryに変換する
              xml = [doc.rootElement convertDictionary];
              // ブログ情報の配列を取得する
              NSArray *blogsJSON = xml[@"rss"][@"channel"][@"item"];
              
              // ブログ情報(NSDictionary)の内容をASBlogエンティティにパースして渡す
              if (block) block([weakSelf parseBlogs:blogsJSON], nil);
              
          } else {
              if (block) block(nil, parseError);
          }
          
      } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
          if (block) block(nil, error);
      }];
}

- (NSArray *)parseBlogs:(NSArray *)blogs
{
    NSMutableArray *mutableBlogs = [NSMutableArray array];
    
    [blogs enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
        ASBlog *blog = [[ASBlog alloc] initWithJSONDictionary:obj];
        [mutableBlogs addObject:blog];
    }];
    
    return [mutableBlogs copy];
}

@end
```

下記は`ASBlog`エンティティの内容です。
初期化時に引数の`NSDictionary`の内容を各プロパティに反映しています。

```objc
@interface ASBlog : NSObject

@property (nonatomic, strong) NSString *title;

@property (nonatomic, strong) NSString *body;

- (instancetype)initWithJSONDictionary:(NSDictionary *)json;

@end

@implementation ASBlog

- (instancetype)initWithJSONDictionary:(NSDictionary *)json
{
    self = [super init];
    
    if (self) {
        // title
        NSString *title = json[@"title"];
        if ([title isKindOfClass:[NSString class]]) {
            self.title = title;
        }
        
        // body
        NSString *body = json[@"description"];
        if ([body isKindOfClass:[NSString class]]) {
            self.body = body;
        }
    }
    
    return self;
}

```

下記が実際のテストコードです。
テストケースはJSON内の各項目と期待値が一致するかで判定しています。

```objc
#import <XCTest/XCTest.h>
#import "AFHTTPRequestOperationManager.h"
#import "ASBlogManager.h"
#import "ASBlog.h"
// Test support
#import <OCMock/OCMock.h>

@interface ASBlogManagerTests : XCTestCase

@end

@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
{
    AFHTTPRequestOperationManager* operationManager = [AFHTTPRequestOperationManager manager];
    // Mockを生成
    id mockOperationManager = [OCMockObject partialMockForObject:operationManager];
    // [AFHTTPRequestOperationManager manager]の呼び出しでmockOperationManagerを返すようにする
    [[[mockOperationManager stub] andReturn:mockOperationManager] manager];
    
    [[mockOperationManager expect]
     GET:@"http://blog.hatena.ne.jp/-/hotentry/rss" parameters:nil
     success:[OCMArg checkWithBlock:^BOOL(void (^successBlock)(AFHTTPRequestOperation *, id))
              {
                  // テストケースクラスが置かれている場所のNSBundleを取得
                  NSBundle *bundle = [NSBundle bundleForClass:self.class];
                  // テストデータファイルのパス
                  NSString *testDataPath = [bundle pathForResource:@"BlogList" ofType:@"txt"];
                  // reponseObjectにテストデータを設定
                  NSData *responseObject = [NSData dataWithContentsOfFile:testDataPath];
                  
                  successBlock(nil, responseObject);
                  
                  return YES;
              }]
     failure:OCMOCK_ANY];
    
    // テスト実行
    ASBlogManager *manager = [ASBlogManager sharedManager];
    
    [manager fetchBlogsWithCompletion:^(NSArray *results, NSError *error) {
        
        XCTAssertTrue(results.count == 2);
        
        ASBlog *blog1 = results[0];
        XCTAssertTrue([blog1.title isEqualToString:@"タイトル1"]);
        XCTAssertTrue([blog1.body isEqualToString:@"本文test1"]);
        
        ASBlog *blog2 = results[1];
        XCTAssertTrue([blog2.title isEqualToString:@"タイトル2"]);
        XCTAssertTrue([blog2.body isEqualToString:@"本文test2"]);
    }];
    
    [mockOperationManager verify];
}

@end
```

## まとめ
今回は`OCMock`を使う事で単体テストを行いました。
同様に[OHHTTPStubs](https://github.com/AliSoftware/OHHTTPStubs)を利用することでも単体テストを実施できそうですが、それは今後チャレンジする予定です。
テストフレームワークに関するよりよい情報などありましたら是非お知らせください。