iOSのUnitTest用モックサーバーをYAMLとJSONから作る


[ニフティクラウド mobile backend（mBaaS）](http://mb.cloud.nifty.com)のSDKテスト用に、
YAMLとJSONファイルでモックサーバーが定義できれば、
AndroidでもiOSでも使いまわせるという作戦です。

前回の[AndroidのUnitTest用モックサーバーをYAMLとJSONから作る](http://qiita.com/yuki_okawa/items/d2aefd78cbc038ed32aa)のをiOSにも展開しました

Androidのほうはクエリストリングとリクエストボディで
レスポンスを切り替える改修もしているので別途どこかで共有します

## YAMLを扱うライブラリ

- [mirek/YAML.framework](https://github.com/mirek/YAML.framework)を利用する

```
target :YOUR_TARGET_NAME do
  pod 'Specta'
  pod 'Expecta'
  pod 'OHHTTPStubs'
  pod 'YAML-Framework'
end
```

## テストコード

- [SpectaやらOHHTTPStubsなどを使って](http://qiita.com/yuki_okawa/items/1586375581ea8cc510fa)います
- beforeAllでYAMLとJSONファイルを読み込みます
 - 基本はYAMLファイルと一致するものをfor文で探しているだけです

### ポイント

- SpectaがXCTestベースなので、bundleを取得するときにmainBundleだとうまくいかない
 - @keroxpさんの[XCTestの中でNSBundleにアクセスする](http://qiita.com/keroxp/items/e372693c1478f1b57343)を参考にしています

```objective-c
NSString *path = [[NSBundle bundleForClass:[self class]] pathForResource:@"mbaas" ofType:@"yml"];
NSData *ymlData = [NSData dataWithContentsOfFile:path];
```

- OHHTTPStubsのレスポンス作成は、都度stubRequestsPassingTest:withStubResponse:メソッドを呼び出さない方法にしました
 - @yimajoさんの[OHHTTPStubsを使ったiOSアプリ開発の進め方](http://qiita.com/yimajo/items/b30799d68302e17a172b)を参考にしてます

### YAMLとJSONファイル

- resource/yamlグループとresource/jsonグループをXcodeでつくりましたが、あんまり関係ないです
 - Build PhasesのCopy Bundle Resourcesに追加されていればOKだと思います

YAMLファイル

```
---
  request:
    url: /2013-09-01/classes/TestClass/testObjectId
    method: GET
  response:
    status: 201
    file: valid_get_request.json
```

JSONファイル

```
{"objectId":"testObjectId","createDate":"2015-08-05T00:00:00.000Z","updateDate":"2015-08-05T00:00:00.000Z","acl":{"*":{"read":true, "write":true}}}
```

### テストコード全文

```objective-c
#import <Specta/Specta.h>
#import <Expecta/Expecta.h>
#import <OHHTTPStubs/OHHTTPStubs.h>
#import <YAML-Framework/YAMLSerialization.h>

SpecBegin(StubTest)

beforeAll(^{
    
    //Yaml file reading
    NSString *path = [[NSBundle bundleForClass:[self class]] pathForResource:@"mbaas" ofType:@"yml"];
    NSData *ymlData = [NSData dataWithContentsOfFile:path];
    NSError *error = nil;
    NSMutableArray *yaml = [YAMLSerialization objectsWithYAMLData:ymlData
                                                          options: kYAMLReadOptionStringScalars
                                                            error: &error];
    
    //set default response
    NSDictionary *responseDic = @{@"code":@"E404001",
                                  @"error":@"Not found request"};
    NSData *jsonResponse = [NSJSONSerialization dataWithJSONObject:responseDic options:0 error:nil];
    
    //CreateStub
    [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) {
        for (NSDictionary *stub in yaml){
            NSDictionary *stubRequest = [stub objectForKey:@"request"];
            if (![[stubRequest objectForKey:@"url"] isEqualToString:request.URL.path]){
                continue;
            }
            if (![[stubRequest objectForKey:@"method"] isEqualToString:request.HTTPMethod]){
                continue;
            }
            return YES;
        }
        return NO;
    } withStubResponse:^OHHTTPStubsResponse*(NSURLRequest *request) {
        for (NSDictionary *stub in yaml){
            NSDictionary *stubRequest = [stub objectForKey:@"request"];
            if (![[stubRequest objectForKey:@"url"] isEqualToString:request.URL.path]){
                continue;
            }
            if (![[stubRequest objectForKey:@"method"] isEqualToString:request.HTTPMethod]){
                continue;
            }
            NSString *jsonFileName = [[stub objectForKey:@"response"] objectForKey:@"file"];
            NSArray *fileNameArray = [jsonFileName componentsSeparatedByString:@"."];
            NSString *jsonPath = [[NSBundle bundleForClass:[self class]] pathForResource:fileNameArray[0]
                                                                                  ofType:fileNameArray[1]];
            
            return [OHHTTPStubsResponse responseWithData:[NSData dataWithContentsOfFile:jsonPath]
                                              statusCode:[[[stub objectForKey:@"response"] objectForKey:@"status"] intValue]
                                                 headers:@{@"Content-Type":@"application/json"}];;
        }
        return [OHHTTPStubsResponse responseWithData:jsonResponse
                                          statusCode:404
                                             headers:@{@"Content-Type":@"application/json"}];
    }];
});

describe(@"OHHTTPStubs", ^{
    it(@"should return stub response", ^{
        NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"https://example.com/2013-09-01/classes/TestClass/testObjectId"]];
        NSHTTPURLResponse *response = nil;
        NSData *responseData = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:nil];
        NSDictionary *responseDic = [NSJSONSerialization JSONObjectWithData:responseData
                                               options:NSJSONReadingAllowFragments
                                                 error:nil];
        expect([responseDic objectForKey:@"objectId"]).to.equal(@"testObjectId");
    });
});

afterAll(^{
    [OHHTTPStubs removeAllStubs];
});

SpecEnd

```

## さいごに

- デフォルトのレスポンスを作成するのであれば、リクエストをチェックする部分はすぐtrueを返してもいい気がしています。YAMLファイルの中身を2回チェックしているので。。。

```objc
//このfor文が２回実装されている。。。
for (NSDictionary *stub in yaml){

}
```

- （まだ）URLのパスとリクエストメソッドしかチェックしていません
- AndroidもiOSもいつかライブラリにしてしまいたいです