SwiftのAlamofire+CodableでAPIクライアントの作成からUnitTestまでを実装する

概要

iOSエンジニアの通信処理の練習用にこんなAPIを作成しました。 herokuで無料枠でデプロイしていますのでお金はかからないはずです。 その代わりにスリープモードなのでアクセスしてもレスポンスが遅いので、2,3回アクセスして起こしてもらう必要があります。

https://todo-practice-app.herokuapp.com/

スクリーンショット 2018-12-30 20.48.03.png

{"items":[{"id":1,"first_name":"Taro","last_name":"Tanaka","title":"first_data","create_at":"2018-12-30T15:03:01.012345"},{"id":2,"first_name":"Hanako","last_name":"Yamada","title":"second-data","create_at":"2018-12-31T18:11:01.012345"}]}

https://todo-practice-app.herokuapp.com/items

[{"id":1,"first_name":"Taro","last_name":"Tanaka","title":"first_data","create_at":"2018-12-30T15:03:01.012345"},{"id":2,"first_name":"Hanako","last_name":"Yamada","title":"second-data","create_at":"2018-12-31T18:11:01.012345"}]

https://todo-practice-app.herokuapp.com/first

{"id":1,"first_name":"Taro","last_name":"Tanaka","title":"first_data","create_at":"2018-12-30T15:03:01.012345"}

https://todo-practice-app.herokuapp.com/second

{"id":2,"first_name":"Hanako","last_name":"Yamada","title":"second-data","create_at":"2018-12-31T18:11:01.012345"}

上記のURLに接続するとJSONが出力されます。

つまり、こちらがAPIとなります。

これらをそれぞれネイティブ側(iOS)でパースしてみましょう。

環境

ライブラリ・環境 バージョン
Xcode 10.1
Swift 4.2
Alamofire 4.8.0

モデルクラスの設計と実装

今回はモデルクラスの命名Taskにしました。 今回は全てをデコードするのではなくidfirstlastをそれぞれデコードする。

Task.swift
struct Task: Codable {
    let id: Int
    let first: String
    let last: String

    enum CodingKeys: String, CodingKey {
        case id
        case first = "first_name"
        case last = "last_name"
    }
}

https://todo-practice-app.herokuapp.com/easy/XXX

を使うとパラメータと変数名が一致するためCodingKeysが必要なくなります。 こちらは私なりの配慮です。

https://todo-practice-app.herokuapp.com/easy/items

スクリーンショット 2018-12-30 20.34.59.png

https://todo-practice-app.herokuapp.com/easy/first

スクリーンショット 2018-12-30 20.35.45.png

easy版を使う場合

Task.swift
struct Task: Codable {
    let id: Int
    let first: String
    let last: String
}

以上でモデルクラスの作成が完了します。

Controller側でAPIを呼び出してデコードする

ViewController.swift
import UIKit
import Alamofire

class ViewController: UIViewController {

    private var items: [Task]?

    override func viewDidLoad() {
        super.viewDidLoad()
        requestFirst()
        requestItems()
    }

    private func requestFirst() {
        Alamofire.request("https://todo-practice-app.herokuapp.com/first").response { response in
            guard let data = response.data else {
                return
            }
            let decoder = JSONDecoder()
            do {
                let task: Task = try decoder.decode(Task.self, from: data)
                print(task)
            } catch {
                print(error)
            }
        }
    }

    private func requestItems() {
        Alamofire.request("https://todo-practice-app.herokuapp.com/items").response { response in
            guard let data = response.data else {
                return
            }
            let decoder = JSONDecoder()
            do {
                let tasks: [Task] = try decoder.decode([Task].self, from: data)
                print(tasks)
            } catch {
                print(error)
            }
        }
    }
}

requestItems()を呼び出し場合

[TodoApp.Task(id: 1, first: "Taro", last: "Tanaka"), TodoApp.Task(id: 2, first: "Hanako", last: "Yamada")]

requestFirst()を呼び出し場合

MobileGestalt.c:890: MGIsDeviceOneOfType is not supported on this platform.
Task(id: 1, first: "Taro", last: "Tanaka")

さて、ここまではよくQiitaで見かける記事であります。

次にこれのModelのUnitTestを作ります。

ModelクラスのUnitTestを作成する

プロジェクトファイルの構成は下記の通りに実装します。

スクリーンショット 2018-12-30 21.48.21.png

ResourcesフォルダにJSONのオブジェクトを入れる形式が好みです。

one_task.json
{
    "id": 1,
    "first_name": "Taro",
    "last_name": "Tanaka",
    "title": "first_data",
    "create_at": "2018-12-30T15:03:01.012345"
}

テストを書きます。

TestTask.swift
import XCTest
@testable import TodoApp

class TestTask: XCTestCase {

    func test_Task()
    {
        guard let jsonObject = createDataObject() else {
            XCTFail("Dataの生成に失敗")
            return
        }
            let decoder = JSONDecoder()
        guard let task = try? decoder.decode(Task.self, from: jsonObject) else {
            XCTFail("Taskの生成に失敗")
            return
        }
        XCTAssertEqual(task.id, 1)
        XCTAssertEqual(task.first, "Taro")
        XCTAssertEqual(task.last, "Tanaka")
    }

    private func createDataObject() -> Data?
    {
        let testBundle = Bundle(for: type(of: self))
        let path = testBundle.url(forResource: "one_task", withExtension: "json")
        let data = try? Data(contentsOf: path!, options: .uncached)
        return data
    }
}

あとはユニットテストを実行してSuccessが表示されたら完了です。

これで無事にMockからJSONDecoderでCodableでデコードがしっかりできていることがわかりました。