LoginSignup
9
11

More than 5 years have passed since last update.

iOS アプリの Unit Test - OCMock 編

Last updated at Posted at 2016-11-04

以前、Objective-C での Unit Test を紹介しましたが、今回は OCMock というモックライブラリの使い方を紹介します。

Mock とは

モックを使う目的は大きく以下のものになるかと思います。

  • まだ完成していないクラスを代用する。
  • システム日時など都度都度値が変化するものを固定にする。
  • テスト対象から別なクラスのメソッドが呼ばれているか確認する。
  • エラーを発生させることが困難な場合、このエラーを擬似的に発生させる。

導入

CocoaPods から導入します。
メインのターゲットには含めず、テスト用のターゲットに含めます。

Podfile
# 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 をインポートします。

sampleTests.m
#import <OCMock/OCMock.h>

モックオブジェクトの作成

クラス

クラスメソッドを差し替え対象とする場合に使用します。

sampleTests.m
    id sampleMock = OCMClassMock([SampleClass class]);

オブジェクト

インスタンスを作成し、その一部のメソッドを差し替え対象とする場合に使用します。

sampleTests.m
    SampleClass *sampleObject = [[SampleClass alloc] init];
    id sampleMock = OCMPartialMock(sampleObject);

プロトコル

デリゲートなどのプロトコルを対象とする場合に使用します。

sampleTests.m
    id sampleMock = OCMProtocolMock(@protocol(SampleClassDelegate));

戻り値の差し替え

あるメソッドの戻り値を差し替える場合には以下のように andRerurn を使って定義します。
この例では、 stringValueMethod: の戻り値として "ABC" を返すようにしています。
stringValueMethod: の引数として特に指定がない場合は、引数に OCMOCK_ANY を指定します。

sampleTests.m
OCMStub([mock stringValueMethod:OCMOCK_ANY]).andReturn(@"ABC");

引数が特定の値(1)の場合にだけ "ABC" を返す場合は、このように引数に 1 を指定します。

sampleTests.m
OCMStub([mock stringValueMethod:@(1)]).andReturn(@"ABC");

サンプル

sampleTests.m
/*!
 * 戻り値を 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 にて定義した処理が実行されるようにします。

サンプル

sampleTests.m
/*!
 * 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.");
    }];
}

呼び出しの確認

対象のメソッドが呼び出されるか確認する場合は、OCMVerifyOCMVerifyAllWithDelay を利用します。
単純にメソッドが呼び出されたのかを確認する場合は、OCMVerify を利用し、非同期処理の完了を受けて呼び出される場合は、 OCMVerifyAllWithDelay を利用します。
また、非同期処理の場合は OCMExpect で呼び出されるメソッドを事前に定義しておきます。

サンプル

sampleTests.m
/*!
 * 処理が呼ばれたか確認する。
 */
- (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 ではなく、確認したい値とすれば実現できます。

sampleTests.m
- (void)testMockSamleProtocol {
    id sampleMock = OCMProtocolMock(@protocol(SampleClassDelegate));

    SampleClass *sampleObject = [[SampleClass alloc] init];
    sampleObject.delegate = sampleMock;
    [sampleObject callDelegate];

    OCMVerify([sampleMock sampleClass:sampleObject returnValue:@(YES)]);
}

もう少し細かく検証したい場合や、検証したい値に幅がある場合は、OCMArg を利用します。

sampleTests.m
- (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

9
11
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
9
11