iOS
AFNetwork
kiwi

Kiwiテストで非同期メソッドの引数に渡したBlockを実行する部分のスタブ化

More than 5 years have passed since last update.

具体的な例として、AFNetworking(2.0)でログインAPIを叩くんだけどテスト時に通信を行わないようなサンプルを書いた。

  • 外部への通信が不要になる
  • 実パスワードをテストに埋め込まなくてよくなる
  • NSURLProtocolで直接レスポンスを書き換えるテストより柔軟

最終形

HANAPIClient.m
@interface HANAPIClient()
@property(nonatomic, strong) AFHTTPRequestOperationManager *request;
@end

- (void)login:(NSString *)name password:(NSString *)password completionHandler:(void (^)(NSError *errorOrNil))completionHandler
{
  [self.request POST:@"https://www.example.jp/login"
          parameters:@{@"name": name, @"password": password}
             success:^(AFHTTPRequestOperation *operation, id responseObject) {
                        completionHandler(nil);
             }
             failure:^(AFHTTPRequestOperation *operation, NSError *error) {
                        completionHandler(error);
             }];
}
MyTests.m
#import <Kiwi/Kiwi.h>

#import <AFNetworking/AFNetworking.h>
#import "HANAPIClient.h"

SPEC_BEGIN(HANAPIClientTests)

describe(@"HANAPIClientTests", ^{
  __block HANAPIClient *client;
  beforeEach(^{
    client = [HANAPIClient new];
  });
  it(@"ログインする", ^{
    __block BOOL success = NO;
    id req = [AFHTTPRequestOperationManager mock];
    id block = ^(NSArray* params){
      void (^success)(AFHTTPRequestOperation *operation, id responseObject) = params[2];
      success(nil, nil);
    };
    [req stub:@selector(POST:parameters:success:failure:) withBlock:block];
    [client setValue:req forKey:@"request"];
    [client login:@"-----------hogehoge------------" password:@"hoehogehoge" completionHandler:^(NSError *errorOrNil) {
      success = (errorOrNil == nil);
    }];

    [[expectFutureValue(theValue(success)) shouldEventually] beTrue];
  });
});

SPEC_END

解説

it(@"ログインする", ^{
  __block BOOL success = NO;
  [client login:@"-----------hogehoge------------" password:@"hoehogehoge" completionHandler:^(NSError *errorOrNil) {
    success = (errorOrNil == nil);
  }];

  [[expectFutureValue(theValue(success)) shouldEventually] beTrue];
});

モック化する前の基本的な非同期メソッドのテスト(https://github.com/allending/Kiwi/wiki/Asynchronous-Testing )

it(@"ログインする", ^{
  __block BOOL success = NO;

  id req = [AFHTTPRequestOperationManager mock];
  [client setValue:req forKey:@"request"];

  [client login:@"-----------hogehoge------------" password:@"hoehogehoge" completionHandler:^(NSError *errorOrNil) {
    success = (errorOrNil == nil);
  }];

  [[expectFutureValue(theValue(success)) shouldEventually] beTrue];
});

HANAPIClientのプライベートなプロパティのrequestをモックに置き換える。

テスト時にだけ@interfaceを宣言し直す方がわかりやすい(Objective-Cのテストクラスからプライベートメソッド/プロパティを参照したい)、けどsetValue:forKey のが楽なので使ってる。

it(@"ログインする", ^{
  __block BOOL success = NO;

  id req = [AFHTTPRequestOperationManager mock];

  id block = ^(NSArray* params){
    void (^success)(AFHTTPRequestOperation *operation, id responseObject) = params[2];
    success(nil, nil);
  };  
  [req stub:@selector(POST:parameters:success:failure:) withBlock:block];

  [client setValue:req forKey:@"request"];

  [client login:@"-----------hogehoge------------" password:@"hoehogehoge" completionHandler:^(NSError *errorOrNil) {
    success = (errorOrNil == nil);
  }];

  [[expectFutureValue(theValue(success)) shouldEventually] beTrue];
});

内部で利用しているAFHTTPRequestOperationManagerのPOST:parameters:success:failureメソッドをスタブ化する。

このメソッドのふるいまいでスタブ化したいのは返り値じゃなくて、呼び出し時にsuccess/failureのBlockが呼ばれる部分なのでKWMockのstub:withBlockでsuccessを必ず呼び出すようなBlockを渡す。