こんにちは。VOYAGE GROUPのエンジニアのジャニーです。
サーバーサイド開発、iOSアプリ開発等に業務で携わっています。
今回は最近取り組みましたiOSアプリにおけるユニットテストについて書きたいと思います。
事前知識
Xcodeにはデフォルトでユニットテストを行うフレームワーク(XCTest)が組み込まれておりApple社からドキュメントも提供されています。
テストをする上で困った事
テストが重要だと言うのは認識していても、テストをしやすい仕組みがなければコストも掛かるし気分も乗らないですよね。
そういった意味ですぐ欲しくなった仕組みを書いてみます。
- Mock
- iOSアプリでのテスト問わず、シンプルにクラスのテストをしたい場合、非常に便利ですよね。
- 非同期処理のテスト
- Xcode6~ではデフォルト機能として非同期処理のテスト用APIが追加されていますが、XCTestExpectationも中々動作が掴み辛かった(後述)ので、もっとシンプルな物はないのか、、と。
- API通信部分のテスト
- 通信に依存しないテストがしたい、
- テストに限らない、開発を進める上で通信周りで使いやすいStubがあればAPI側の開発に依存せず開発を進められて便利。
解決を考える
言語やプラットフォームが違っても開発者が悩む事って大体同じだよね、という事で、便利なライブラリが無いか探してみました。
- Mock
- OCMock
- 情報量が多く使い方がシンプルでverify、partialMockも使えるし自分の求める機能としては十分でした。
- OCMock
- 非同期処理のテスト
- TKRGuard
- TRVSMonitor
- 非同期テストの為だけにKiwiやGHUnitを導入するのもなんだな、と思い単機能のOSSライブラリを探してみました。
- usageを見た感じ直感的に使いやすそう。コード量も少ないので何かあっても読めば良いかな気分。
- API通信部分のテスト
- OHHTTPStubs
- NSURLSession、NSURLRequestの通信にフックしてstubの内容を返す事が出来る。
- 通信周りでAFNetworkingのようなライブラリを使っていてもstub化が容易
- レスポンスタイムの指定が出来るので、タイムアウトのテストが出来る
上記のライブラリはCocoaPodsを使っていれば、Podfileを1行書くだけで簡単に導入が可能です。
試してみよう
CocoaPodsで各ライブラリを導入し、ヘッダファイルを読み込みます。
#import "OHHTTPStubs.h" #import "TKRGuard.h" #import "OCMock.h" #import "TRVSMonitor.h"
OCMock利用サンプル
OCMockを使ったサンプルです。TestUserクラスをMock化し、getUserTypeが呼ばれた際の差し替え、呼び出しの保証を行っています。
TestUser.m
@implementation TestUser - (NSString *) getUserType { return @"female"; } @end
TestCampaign.m
@implementation TestCampaign - (BOOL) isTargetUser:(TestUser *)user { NSString *testTarget = @"male"; if ([testTarget isEqualToString:[user getUserType]]) { return YES; } return NO; } @end
テストコード
id userMock = [OCMockObject mockForClass:[TestUser class]]; [[[userMock expect] andReturn:@"male"] getUserType]; TestCampaign *campaign = [TestCampaign new]; BOOL result = [campaign isTargetUser:userMock]; [userMock verify]; XCTAssertTrue(result);
非同期テスト サンプル
以下はblock内でXCTFailを発行する事でテストを失敗させる事を期待していますが、block内の処理を待たずテストが終わる為、テストが成功してしまいます。
TestClass *test = [TestClass new]; [test test:^(id response) { XCTFail(@"failed"); }]; XCTAssert(YES, @"Pass");
Xcode6~で追加されたXCTestExpectationを使う場合です。
XCTestExpectation *expect = [self expectationWithDescription:@"test"]; TestAPI *api = [TestAPI new]; NSString *expected = @"pass1"; __block NSString *result = @""; [api testGET:^(id response) { [expect fulfill]; result = @"pass1"; }]; //fullfillが終わるまで待機される [self waitForExpectationsWithTimeout:5.0 handler:^(NSError *error) { //timeoutもしくはfullfill後に呼ばれるblock NSLog(@"end expect1"); }]; XCTAssertTrue([expected isEqualToString:result]); XCTestExpectation *expect2 = [self expectationWithDescription:@"test2"]; expected = @"pass2"; [api testGET:^(id response) { [expect2 fulfill]; result = @"pass2"; }]; //fullfillが終わるまで待機される [self waitForExpectationsWithTimeout:5.0 handler:^(NSError *error) { //timeoutもしくはfullfill後に呼ばれるblock NSLog(@"end expect2"); }]; XCTAssertTrue([expected isEqualToString:result]);
1つのXCTestExpectationに対してfullfillが複数回呼ばれるとAPI Violationが起きる、タイムアウト後にfullfillが呼ばれると落ちる、等がありハマりました。
使用する際は、ヘッダーファイルのコメントを一度確認した方が良いと思います。
さてTRVSMonitorではsignal、waitの2つのメソッドでコントロールできます。
直感的で分かりやすいですね。
TRVSMonitor *monitor = [TRVSMonitor monitor]; TestAPI *api = [TestAPI new]; NSString *expected = @"pass1"; __block NSString *result = @""; [api testGET:^(id response) { result = @"pass1"; [monitor signal]; }]; [monitor wait]; XCTAssertTrue([expected isEqualToString:result]); expected = @"pass2"; [api testGET:^(id response) { result = @"pass2"; [monitor signal]; }]; [monitor wait]; [monitor signal];//複数回呼んでも落ちない。 XCTAssertTrue([expected isEqualToString:result]); XCTAssert(YES, @"Pass");
これでXCTFailによってテストが失敗する事が確認出来ました。
TKRGuardの場合はRESUME、WAITのマクロのみで同じ事が出来ます。
TKRGuardでは処理再開時にステータスを渡す事も出来ますし、マクロを利用せずTRVSMonitorと同じような使い方も可能です。
TestAPI *api = [TestAPI new]; [api testGET:^(id response) { XCTFail(@"failed"); RESUME; }]; WAIT; RESUME;//特に落ちたりはしない。本来は不要 XCTAssert(YES, @"Pass");
公式的にXCTestExpectationが用意されていますが、個人的には記載が簡潔で済み、コード量としても多くなく把握しやすいTKRGuardが今時点では使い勝手が良かったです。
OHHTTPStubs 利用サンプル
以下は、www.hogehoge.co.jp、www.hogehoge.comに通信するTestAPIクラスの通信をフックしwithStubResponse:^OHHTTPStubsResponse(NSURLRequest request)内に設定した値を返している例です。
//request内容でStub化するかを決める(YESならwithStubResponse:^OHHTTPStubsResponse*(NSURLRequest *request) blockの処理に移る) [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { if ([request.URL.absoluteString isEqualToString:@"http://www.hogehoge.co.jp/"]){ return YES; } else if ([request.URL.absoluteString isEqualToString:@"http://www.hogehoge.com"]){ return YES; } return NO; } withStubResponse:^OHHTTPStubsResponse*(NSURLRequest *request) { return [OHHTTPStubsResponse responseWithJSONObject:@{@"RESULT":@"pass"} statusCode:200 headers:@{@"Content-Type":@"application/json"}]; }]; TestAPI *api = [TestAPI new]; __block NSString *result = @"failed"; [api testGET:^(id response) { NSLog(@"%@",response); if ([response isKindOfClass:[NSError class]]) { result = @"pass"; RESUME; return; } result = response[@"RESULT"]; RESUME; }]; WAIT; XCTAssertTrue([result isEqualToString:@"pass"]); [OHHTTPStubs removeAllStubs]; }
まとめ
簡単に使ってみた感想を個別にまとめました。
OCMock
- 公式のドキュメントも充実しているし、シンプルで使いやすいと思いました。呼び出し回数の担保をする仕組みが今回調べた範囲だと見当たらなかったので、そこら辺があれば文句無しだと思います。
XCTestExpectation
- 複数回使う場合の挙動でハマったりした所もあり、TRVSMonitor、TKRGuardに比べて事前に理解しないといけない事が多いな、とは思います。公式APIなので、今後改善されていく所もあると思いますので期待ですね。
TRVSMonitor
- TKRGuardに比べて毎回インスタンス生成をするのが手間ですが機能的には十分だと思います。
TKRGuard
- マクロで簡潔に書けるので、用途としては一番使いやすかったです。
OHHTTPStubs
- 機能的には十分だと思いました。ステータスコード、レスポンスタイムが操作出来るのが便利です。
上記を使う事で、テストで悩む事が大幅に減りました。
テストコードは書きやすく、悩む事が少ないにこした事は無いと思いますので、テストをもっとしやすくしたい方は是非試してみてください。
今後やってみたい、書いてみたい事
今回はユニットテストの範囲で便利なライブラリに関して記事を書いてみました。 機会があれば、次回はUI Automationあたりを試してみて記事を書いてみようと思います。