OCMockでNSURLSessionのテストをする


HTTPクライアント通信のテストをするにはOHHTTPStubsという便利なスタブツールライブラリがありますが、現在POSTメソッドで送るBodyのテストを簡単にすることははできません。

（AFNetworkingとNSURLProtocolを使って擬似的な方法で行う方法はwikiに書いてあります。 https://github.com/AliSoftware/OHHTTPStubs/wiki/Testing-for-the-request-body-in-your-stubs ）

以下はNSURLSessionの特定の使い方をしている場合に限定した、OCMockを使って簡単にテストする方法のメモです。

## テスト対象

テスト対象コードはこんな感じ

```objective-c
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:WEB_API_URL]];
    request.HTTPMethod = @"POST";
    
    request.HTTPBody = requestBodyData;
    
    NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
    sessionConfig.TLSMaximumSupportedProtocol = kTLSProtocol12;
    sessionConfig.TLSMinimumSupportedProtocol = kTLSProtocol1;
    NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfig
                                                          delegate:nil
                                                     delegateQueue:[NSOperationQueue mainQueue]];
    
    __weak typeof(self) wself = self;
    [[session dataTaskWithRequest:request
                completionHandler:^(NSData *data, NSURLResponse *response, NSError *connectionError) {
         if (!wself) { return; }
         
         // 完了時の処理
         
     }] resume];

```

## Stub作成

乱暴ですが、NSURLSessionとNSURLSessionTaskをモックして、dataTaskWithRequest:completionHandler:とresumuをstub化するメソッドcreateNSURLSessoinStub()を用意します。

```objective-c

// モックオブジェクトを返すメソッド
// 引数はrequestのチェック用ブロックと、レスポンスを返すメソッド
// レスポンスはOHHTTPStubsResponseを利用
id createNSURLSessoinStub(
    BOOL (^requestCheck)(NSURLRequest *request),
    OHHTTPStubsResponse* (^responseCreator)(NSURLRequest *request, NSData* requestBody))
{
    
    id urlSession = OCMClassMock([NSURLSession class]);
    // クラスメソッドの置き換え
    OCMStub([urlSession sessionWithConfiguration:OCMOCK_ANY
                                        delegate:nil
                                   delegateQueue:[NSOperationQueue mainQueue]])
        .andReturn(urlSession);
    
    OCMStub([urlSession dataTaskWithRequest:[OCMArg checkWithBlock:requestCheck]
                          completionHandler:OCMOCK_ANY])
    .andDo(^(NSInvocation *invocation)
           {
               __unsafe_unretained NSURLRequest *request;
               __unsafe_unretained void (^completionHandler)(NSData *data, NSURLResponse *response, NSError *error);
               
               [invocation getArgument:&request atIndex:2];
               [invocation getArgument:&completionHandler atIndex:3];
               
               __block id urlSessionTask = OCMClassMock([NSURLSessionDataTask class]);
               
               OCMStub([urlSessionTask resume])
               .andDo(^(NSInvocation *invocation)
                      {
                          NSData *requestBody = request.HTTPBody;
                          OHHTTPStubsResponse* stubResponse = responseCreator(request, requestBody);
                          
                          NSURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:request.URL
                                                                                statusCode:stubResponse.statusCode
                                                                               HTTPVersion:@"HTTP/1.1"
                                                                              headerFields:stubResponse.httpHeaders];
                          NSData *body = nil;
                          if (stubResponse.dataSize > 0) {
                              NSMutableData *bodyBuffer = [NSMutableData dataWithLength:stubResponse.dataSize];
                              [stubResponse.inputStream open];
                              NSInteger readBytes = [stubResponse.inputStream read:bodyBuffer.mutableBytes
                                                                         maxLength:bodyBuffer.length];
                              [stubResponse.inputStream close];
                              if (readBytes > 0) {
                                  body = bodyBuffer;
                              }
                          }
                          completionHandler(body?:[NSData data], response, stubResponse.error);
                      });
               
               [invocation setReturnValue:&urlSessionTask];
           });
    
    return urlSession;
}
```

これでOHHTTPStubsのstubRequestsPassingTest:withStubResponse:と同じイメージで使えます。

## 使い方

使い方の例

```objective-c
// Json用にラップするヘルパーメソッド
id createNSURLSessoinStubWithBodyJson(
    BOOL (^requestCheck)(NSURLRequest *request),
    OHHTTPStubsResponse* (^responseCreator)(NSURLRequest *request, NSDictionary* requestBodyJson))
{
    return createNSURLSessoinStub(requestCheck,
                                  ^OHHTTPStubsResponse*(NSURLRequest *request, NSData* requestBody) {
                                      NSError *error = nil;
                                      id obj = [NSJSONSerialization JSONObjectWithData:requestBody options:0 error:&error];
                                      if (!obj || error != nil) {
                                          return [OHHTTPStubsResponse responseWithData:[@"fake server error" dataUsingEncoding:NSASCIIStringEncoding]
                                                                            statusCode:500
                                                                               headers:@{}];
                                      }
                                      
                                      return responseCreator(request, obj);
                                  });
}

// テストの実体
- (void)testHogeHogeApi {
    
    // stubの準備
    createNSURLSessoinStubWithBodyJson(
        ^BOOL(NSURLRequest *request) {
            // リクエストのチェック
            return [request.HTTPMethod isEqualToString:@"POST"]
            && [request.URL.path hasSuffix:@"/hogehoge"];
              
        },^OHHTTPStubsResponse*(NSURLRequest *request, NSDictionary* requestJson) {

            // ここでリクエストボディをチェック
            if (![requestJson[@"id"] isEqual:@(1)] ||
                ![requestJson[@"hogetype"] isEqual:@"fuga"]) {
                
                return [OHHTTPStubsResponse responseWithData:nil statusCode:500 headers:nil];
            }
                        
            // レスポンスを返す
            return [OHHTTPStubsResponse responseWithJSONObject:@{@"time":@"2006-01-02T15:04:05Z07:00"}
                                                    statusCode:200
                                                       headers:@{}];
        }); 
        
    // テストの実行
}

```

## 注意点

NSURLSessionの使い方が変わるとうまく動かなくなります。
テスト対象の実装を限定するようなテストはスジが悪いと思われますので、よく考えてお使いください。