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