炊きたてのご飯が食べたい

定時に帰れるっていいね。自宅勤務できるっていいね。子どもと炊きたてのご飯が食べられる。アクトインディでは積極的にエンジニアを募集中です。

Swift2 - Quick + Mockingjay で Alamofire の非同期通信のテストをする


API との通信をテストする時、実際にリクエストを投げてしまうとサーバーへの負荷やテスト時間など、コストが多くかかってしまうので、Alamofire のリクエストを Quick と Mockingjay を使ってスタブする方法を紹介したいと思います。

  • Quick: RSpec 風に書ける単体テストライブラリ Quick
  • Mockingjay: URL リクエストをモックできるライブラリ Mockingjay

以前書いた Swift2 Alamofire + ObjectMapper で API クライアントを作成する のリクエストに対してテストを書いてみたいと思います。テストするリクエストは以下です。

API.call(Endpoint.User.Find(id: 1)) { response in
  switch response {
  case .Success(let result):
      print("success \(result)")
  case .Failure(let error):
      print("failure \(error)")
  }
}
import Alamofire
import ObjectMapper

class Endpoint {
    enum User: RequestProtocol {
        typealias ResponseType = UserEntity
        
        case Find(id: Int)
        
        var method: Alamofire.Method {
            switch self {
            case .Find:
                return .GET
            }
        }
        
        var path: String {
            switch self {
            case .Find(let id):
                return "/users/\(id)"
            }
        }
    }
}
import ObjectMapper

class UserEntity: Mappable {
    var id: Int?
    var name: String?
    var email: String?
    var url: String?
    
    required convenience init?(_ map: Map) {
        self.init()
    }
    
    func mapping(map: Map) {
        id <- map["id"]
        name <- map["name"]
        email <- map["email"]
        url <- map["url"]
    }
}
  1. テストを書く
  2. スタブ用の json ファイルを用意する
  3. json ファイルから NSDate に変換する処理は繰り返し利用する為 Helper クラスを用意する

1. テストを書く

テストの完成形は以下の様になります。

  • Quick には toEventually という非同期の処理様のテストメソッドがあるのでそれを利用します
  • Class のプロパティとしてテスト対象のデータを宣言しないと上手くtoEventually メソッドが使えません
  • self.stub のメソッドが Mockingjay によるスタブです。 jsonData で指定した data がレスポンスとして返ります

  • UserSpec

import Quick
import Nimble
import Mockingjay
import ObjectMapper
@testable import projectName

class UserSpec: QuickSpec {
    var user: UserEntity?
    var stubUser: UserEntity?
    var error: NSError?
    
    override func spec() {
        describe("Endpoint.User.Find") {
            context("正常系") {
                beforeEach {
                    let data = SpecHelper.readJSONFile("parent")
                    self.stub(http(.GET, uri: "/users/1"), builder: jsonData(data))
                    let JSONString = String(data: data, encoding: NSUTF8StringEncoding)
                    self.stubUser = Mapper<UserEntity>().map(JSONString)
                }
                afterEach {
                    self.user = nil
                    self.stubUser = nil
                    self.removeAllStubs()
                }
                
                it("指定したユーザー情報が取得できること") {
                    API.call(Endpoint.User.Find(id: 1)) { response in
                        switch(response) {
                        case .Success(let result):
                            self.user = result
                        case .Failure(let error):
                            print(error)
                        }
                    }
                    expect(self.user!.id).toEventually(equal(self.stubUser!.id))
                    expect(self.user!.name).toEventually(equal(self.stubUser!.name))
                    expect(self.user!.email).toEventually(equal(self.stubUs!.email))
                    expect(self.user!.url).toEventually(equal(self.stubUs!.url))
                }
            }
            context("異常形") {
                context("APIがメンテナンス中") {
                    beforeEach {
                        self.stub(http(.GET, uri: "/users/1"), builder: http(503))
                    }
                    afterEach {
                        self.removeAllStubs()
                    }
                    
                    it("503 エラーが返却されること") {
                        API.call(Endpoint.PutFavorite(id: facilityId)) { response in
                            switch response {
                            case .Success(_):
                                break
                            case .Failure(let error):
                                self.error = error
                            }
                        }
                        expect(self.error?.userInfo.description).toEventually(equal("[NSLocalizedFailureReason: Response status code was unacceptable: 503]"))
                        expect(self.error?.code).toEventually(equal(-6003))
                    }
                }
            }
        }        
    }
}

2. スタブ用の json ファイルを用意する

let data = SpecHelper.readJSONFile("parent")
self.stub(http(.GET, uri: "/users/1"), builder: jsonData(data))

Mockingjay では NSData を jsonData に渡してあげることで uri に指定したリクエストをスタブし data をレスポンスとして返してくれます。 json データは記述が多くなることが多いのでレスポンス用の json ファイルを作成します。

  • user.json
{
    "id": 1,
    "name": "ティナ・ブランフォード",
    "email": "tina@example.com",
    "url": "https://ja.wikipedia.org/wiki/ファイナルファンタジーVI"
}

3. json ファイルから NSDate に変換する処理は繰り返し利用する為 Helper クラスを用意する

用意しておくと便利です。

  • SpecHelper.swift
static func readJSONFile(name: String) -> NSData {
    let path: String? = NSBundle(forClass: self).pathForResource(name, ofType: "json")
    let fileHandle: NSFileHandle? = NSFileHandle(forReadingAtPath: path!)
    let data: NSData! = fileHandle?.readDataToEndOfFile()
    
    return data
}