以前、Objective-C での Unit Test を紹介しましたが、今回は OCMock
というモックライブラリの使い方を紹介します。
Mock とは
モックを使う目的は大きく以下のものになるかと思います。
- まだ完成していないクラスを代用する。
- システム日時など都度都度値が変化するものを固定にする。
- テスト対象から別なクラスのメソッドが呼ばれているか確認する。
- エラーを発生させることが困難な場合、このエラーを擬似的に発生させる。
導入
CocoaPods から導入します。
メインのターゲットには含めず、テスト用のターゲットに含めます。
# Uncomment this line to define a global platform for your project
# platform :ios, '9.0'
target 'sample' do
# Uncomment this line if you're using Swift or would like to use dynamic frameworks
# use_frameworks!
# Pods for sample
target 'sampleTests' do
inherit! :search_paths
# Pods for testing
pod 'OCMock'
end
target 'sampleUITests' do
inherit! :search_paths
# Pods for testing
pod 'OCMock'
end
end
使い方
このように OCMock をインポートします。
#import <OCMock/OCMock.h>
モックオブジェクトの作成
クラス
クラスメソッドを差し替え対象とする場合に使用します。
id sampleMock = OCMClassMock([SampleClass class]);
オブジェクト
インスタンスを作成し、その一部のメソッドを差し替え対象とする場合に使用します。
SampleClass *sampleObject = [[SampleClass alloc] init];
id sampleMock = OCMPartialMock(sampleObject);
プロトコル
デリゲートなどのプロトコルを対象とする場合に使用します。
id sampleMock = OCMProtocolMock(@protocol(SampleClassDelegate));
戻り値の差し替え
あるメソッドの戻り値を差し替える場合には以下のように andRerurn
を使って定義します。
この例では、 stringValueMethod:
の戻り値として "ABC" を返すようにしています。
stringValueMethod:
の引数として特に指定がない場合は、引数に OCMOCK_ANY
を指定します。
OCMStub([mock stringValueMethod:OCMOCK_ANY]).andReturn(@"ABC");
引数が特定の値(1)の場合にだけ "ABC" を返す場合は、このように引数に 1 を指定します。
OCMStub([mock stringValueMethod:@(1)]).andReturn(@"ABC");
サンプル
/*!
* 戻り値を Mock で差し替える。
*/
- (void)testMockUserData {
// 戻り値を差し替える
id dateMock = OCMClassMock([NSDate class]);
// [NSDate date] が呼ばれたら、 [NSDate dateWithTimeIntervalSince1970:0] を返す。
OCMStub([dateMock date]).andReturn([NSDate dateWithTimeIntervalSince1970:0]);
NetworkManager *manager = [[NetworkManager alloc] init];
NSDictionary *userData = [manager userData];
XCTAssertNotNil(userData);
XCTAssertNotNil(userData[UserDataUsername]);
XCTAssertEqualObjects(userData[UserDataUsername], @"TEST1234");
XCTAssertNotNil(userData[UserDataUUID]);
XCTAssertEqualObjects(userData[UserDataUUID], @"CAD34831-E763-45A9-8BA2-31991DCB682B");
XCTAssertNotNil(userData[UserDataLatestAccessDate]);
// UserDataLatestAccessDate にシステム日時を設定していた場合、
// [manager userData] が実行されるタイミングとテストを実行するタイミングはことなるが、
// [NSDate date] は同じ値が返るようにしているため、これはエラーにはならない。
XCTAssertEqualObjects(userData[UserDataLatestAccessDate], [NSDate date]);
}
- (void)testMockCurrentDateString {
// Mock 用のインスタンスを作成
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
// インスタンスから Mock オブジェクトを作成
id formatterMock = OCMPartialMock(dateFormatter);
// インスタンスを Mock 用のものに差し替える
OCMStub([[formatterMock alloc] init]).andReturn(dateFormatter);
OCMStub([formatterMock stringFromDate:OCMOCK_ANY]).andReturn(@"1980-12-31 12:34:56");
NetworkManager *manager = [[NetworkManager alloc] init];
NSString *currentDate = [manager currentDateString];
XCTAssertEqualObjects(currentDate, @"1980-12-31 12:34:56", @"Date String is wrong.");
}
Blocks の差し替え
Blocks を差し替える場合は、 andDo
を利用し、 NSInvocation
にて定義した処理が実行されるようにします。
サンプル
/*!
* Blocks を Mock で差し替える。
*/
- (void)testMockImageDownload {
NSString *imageUrlString = @"https://api.test.com/Images/IMG_1234.jpg";
NSURL *imageUrl = [NSURL URLWithString:imageUrlString];
// 差し替える Blocks の内容を定義する
void (^invocation)(NSInvocation *) = ^(NSInvocation *invocation) {
__unsafe_unretained void (^handler)(NSData * __nullable data, NSURLResponse * __nullable response, NSError * __nullable error);
// Argument の Index はこんな感じと予想
// 0: インスタンス
// 1: メソッド
// 2: 引数1
// 3: 引数2 ...
[invocation getArgument:&handler atIndex:3];
NSHTTPURLResponse *httpResponse = [[NSHTTPURLResponse alloc] initWithURL:imageUrl statusCode:404 HTTPVersion:@"1.1" headerFields:nil];
handler(nil, httpResponse, nil);
};
id urlSessionMock = OCMClassMock([NSURLSession class]);
// NSURLSession のインスタンスを Mock のものに差し替える
OCMStub([urlSessionMock sessionWithConfiguration:OCMOCK_ANY]).andReturn(urlSessionMock);
// Blocks の内容を差し替える
// [NSURLSession dataTaskWithURL:completionHandler:] が呼ばれたら、 invocation を実行する。
OCMStub([urlSessionMock dataTaskWithURL:OCMOCK_ANY completionHandler:OCMOCK_ANY]).andDo(invocation);
XCTestExpectation *expectation = [self expectationWithDescription:@"MockImageDownloader"];
NetworkManager *manager = [[NetworkManager alloc] init];
[manager downloadImage:imageUrlString completionHandler:^(BOOL result, NSData * _Nullable imageData, NSError * _Nullable error) {
[expectation fulfill];
XCTAssertFalse(result, @"Image downlad failed.");
XCTAssertEqual(error.code, 404, @"Error is not 'Not Found'");
}];
[self waitForExpectationsWithTimeout:60 handler:^(NSError * _Nullable error) {
XCTAssertNil(error, @"has error.");
}];
}
呼び出しの確認
対象のメソッドが呼び出されるか確認する場合は、OCMVerify
や OCMVerifyAllWithDelay
を利用します。
単純にメソッドが呼び出されたのかを確認する場合は、OCMVerify
を利用し、非同期処理の完了を受けて呼び出される場合は、 OCMVerifyAllWithDelay
を利用します。
また、非同期処理の場合は OCMExpect
で呼び出されるメソッドを事前に定義しておきます。
サンプル
/*!
* 処理が呼ばれたか確認する。
*/
- (void)testVerifyMethod {
id dataManagerMock = OCMClassMock([DataManager class]);
// 確認するオブジェクトのインスタンスを差し替える
OCMStub([dataManagerMock sharedInstance]).andReturn(dataManagerMock);
NetworkManager *netowrkMgr = [[NetworkManager alloc] init];
[netowrkMgr callApiWithCommand:ApiTypeLogout parameters:nil completionHandler:nil];
// 処理が呼ばれたか検証
OCMVerify([dataManagerMock deleteUserData]);
}
/*!
* 処理が呼ばれたか確認する。(遅延)
*/
- (void)testVerifyMethodWait {
id dataManagerMock = OCMClassMock([DataManager class]);
OCMStub([dataManagerMock sharedInstance]).andReturn(dataManagerMock);
// 呼び出される処理を定義
OCMExpect([dataManagerMock updateUserData:OCMOCK_ANY]);
NetworkManager *netowrkMgr = [[NetworkManager alloc] init];
[netowrkMgr callApiWithCommand:ApiTypeLogin parameters:nil completionHandler:nil];
// 指定した処理が呼び出されるまで、指定秒数(60秒)待つ
OCMVerifyAllWithDelay(dataManagerMock, 60);
}
/*!
* Delegate メソッドが呼ばれたか確認する。
*/
- (void)testMockSamleProtocol {
id sampleMock = OCMProtocolMock(@protocol(SampleClassDelegate));
SampleClass *sampleObject = [[SampleClass alloc] init];
sampleObject.delegate = sampleMock;
[sampleObject callDelegate];
OCMVerify([sampleMock sampleClass:OCMOCK_ANY returnValue:OCMOCK_ANY]);
}
引数の確認
引数の値を確認する場合、そのパターンが一つだけの場合であれば、先ほどの OCMVerify
で検証したいメソッドの引数を OCMOCK_ANY
ではなく、確認したい値とすれば実現できます。
- (void)testMockSamleProtocol {
id sampleMock = OCMProtocolMock(@protocol(SampleClassDelegate));
SampleClass *sampleObject = [[SampleClass alloc] init];
sampleObject.delegate = sampleMock;
[sampleObject callDelegate];
OCMVerify([sampleMock sampleClass:sampleObject returnValue:@(YES)]);
}
もう少し細かく検証したい場合や、検証したい値に幅がある場合は、OCMArg
を利用します。
- (void)testVerifyArgment {
NSString *bookmarkId = @"ABC123";
NSDictionary *bookmarkData = @{BookmarkDataId: bookmarkId};
DataManager *manager = [DataManager sharedInstance];
id dataManagerMock = OCMPartialMock(manager);
// selectBookmarkData: が呼ばれたときに、引数の内容を検証する。
OCMExpect([dataManagerMock selectBookmarkData:[OCMArg checkWithBlock:^BOOL(id obj) {
XCTAssertNotNil(obj);
XCTAssertTrue([obj isKindOfClass:[NSString class]]);
XCTAssertEqualObjects(obj, @"ABC123");
}]]);
[manager updateBookmarkData:bookmarkData];
OCMVerify([dataManagerMock selectBookmarkData:OCMOCK_ANY]);
}
このように対象となる引数に対して -checkWithBlock:
を埋め込み、この中で値の検証を行います。
また、メソッドが呼ばれなかった場合はエラーとならないので、OCMVerify
にてメソッド自体が呼ばれることも検証します。
最後に
初めの方にも書きましたが、モックを利用すると対象となるクラスの振る舞いを変えることができるので、まだ実装が完了していないクラスが関係しているテストや、エラーを発生させることが難しいなどの複雑なテストが比較的簡単にできるようになります。
参考
http://ocmock.org/
http://qiita.com/YusukeHosonuma/items/09fe9be15007f2870b83
http://qiita.com/nomadmonad/items/e9b3500998ad0b2d3fee