LoginSignup
48
50

More than 5 years have passed since last update.

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

Posted at

概要

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

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

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

手順

OCMockAFNetworkingのインストールには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ファイルに格納します。
中身はこんな感じです。

<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エンティティにパースしています。

#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の内容を各プロパティに反映しています。

@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内の各項目と期待値が一致するかで判定しています。

#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を利用することでも単体テストを実施できそうですが、それは今後チャレンジする予定です。
テストフレームワークに関するよりよい情報などありましたら是非お知らせください。

48
50
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
48
50