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

More than 5 years have passed since last update.


概要

前回の続きで今回はOHHTTPStubsを使用して単体テストを行ってみました。

(テスト対象となるソースコードの概要は前回のコンテンツを参照してください)

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

今回は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通信のほとんどをテストできます。

また今回は使用しませんが、リクエストタイム、レスポンスタイムも調整できます。

@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を使ってブロック内の処理が完了するまで待機し、処理が完了したら処理を再開してテストを実行しています。

@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で単体テストを行う際に少しでも参考になれば幸いです。