GitHub上のiOSプロジェクトでもTravis CI × CodecovでCIしたい!

**2017/01/31 追記**

CodecovでUIテストのカバレッジがきちんと収集されてないな…　と思い調査してみたところ，他のリポジトリでも同様の現象が起こっていたようです．

どうやら，XcodeのIDEでUIテストを起動したときと，`xcodebuild`でCLIでテストを起動したときでは挙動が異なるらしく，それに起因するもののようです．
以下，参照したIssuesです．

* [UITesting not reported - codecov/example-swift](https://github.com/codecov/example-swift/issues/16)
* [Always 0% - nakiostudio/xcov](https://github.com/nakiostudio/xcov/issues/22)

UI Testsの**カバレッジ収集以外**に関しては，UI Testsの起動含めきちんと想定通りの動作をしております．

---
**2017/06/24 追記**

**Xcode9**においてはこの問題は解決したっぽいです．なので，Xcode9をTravis側がサポートしてくれるようになるとUIテストのカバレッジもきちんと収集されるようになるのかな？

* [Issue appears solved in Xcode 9 - iwllyu/UITestCoverageBug](https://github.com/iwllyu/UITestCoverageBug/issues/4#event-1125129900)


# 初めに
今までと違う畑に慣れてくるとCIを回したくなるのがエンジニアの性というもの．タイトルの通り，iOSのプロジェクトを**Travis CI**でテストしてテストカバレッジを**Codecov**で可視化するところまでやります．テストコードは純粋な**XCTest**を使用しますが，細かい文法の説明などは本記事ではしませんのでご注意ください．

[Travic CI][travis]についてももう説明不要かと思いますので割愛します．[Codecov][codecov]はコードカバレッジ可視化サービスの一つで，publicなリポジトリは無制限に，privateなら1つまで無料で使用できます．(欲を言えば[Travis][travis]みたく学生ならばprivateリポジトリも無制限になって欲しい)

他の似たようなサービスとしては[Coveralls][coveralls]がありますが，なんとなく見た目が合わなかったので今回は[Codecov]にしました．特にこれといった比較検証はしていません．


なお，今回使用したテストプロジェクトはGitHubへあげてあります．

* [kagemiku/ios-travis - GitHub](https://github.com/kagemiku/ios-travis)

# 環境
* Xcode 8.1
* Swift 3.0.1
* iOS SDK 10.1
* iPhone7 Simulator
* Carthage 0.18.1


# 手順
## 1. プロジェクトとリポジトリ作成
Xcodeのプロジェクトを作成する際の注意として，**Include Unit Tests**と**Include UI Tests**にチェックを入れて置いたほうが後から手動で追加せずに済むので楽です．

![project.png](https://qiita-image-store.s3.amazonaws.com/0/35230/bff4aa3d-3c0a-864d-363b-a25636a54465.png "project.png")

もしチェックし忘れて作成した場合は，メニューの「File -> New -> Target...」を選択し，**iOS UI Testing Bundle**ないしは**iOS Unit Testing Bundle**を追加すれば各々のファイルが一式追加されます．

![addtarget.png](https://qiita-image-store.s3.amazonaws.com/0/35230/2b9b81eb-1717-c981-ae14-26b060eaa42c.png "addtarget.png")

![testbundles.png](https://qiita-image-store.s3.amazonaws.com/0/35230/ea684027-6987-9f21-a2c9-02638b165a58.png "testbundles.png")

あとは[GitHub][github]で適当なpublicリポジトリを作り，ローカルのリポジトリ内で`git remote add origin git@github.com:username/reponame.git`とかすればOKです．


## 2. XCTestでテストを書く
アプリのコードを書いたらテストコードを書いていきます．

### 2.1 Unit Tests
Unit Testの例として，今回は次のようなクラスを用意しました．

```Swift:Cashier.swift
import Foundation

enum CashierError: Error {
    case insufficientFundsError(shortage: Int)
}

final class Cashier {
    private var balance: Int = 0

    init(balance: Int) {
        self.balance = balance
    }

    func getBalance() -> Int {
        return self.balance
    }

    @discardableResult
    func deposit(amount: Int) -> Int {
        self.balance += amount

        return self.balance
    }

    @discardableResult
    func withdraw(amount: Int) throws -> Int {
        if amount > self.balance {
            throw CashierError.insufficientFundsError(shortage: amount - self.balance)
        }

        self.balance -= amount

        return self.balance
    }
}
```

これのUnit Testのコードはこんな感じになりました．

```Swift:CashierTests.swift
import XCTest
@testable import ios_travis

class CashierTests: XCTestCase {
    
    override func setUp() {
        super.setUp()
        // Put setup code here. This method is called before the invocation of each test method in the class.
    }
    
    override func tearDown() {
        // Put teardown code here. This method is called after the invocation of each test method in the class.
        super.tearDown()
    }

    func testGetBalance() {
        let balance = 10000
        let cashier = Cashier(balance: balance)
        XCTAssertEqual(cashier.getBalance(), balance)
    }

    func testDeposit() {
        let balance = 10000
        let amount  = 1000
        let cashier = Cashier(balance: balance)

        XCTAssertEqual(cashier.deposit(amount: amount), balance + amount)
        XCTAssertEqual(cashier.getBalance(), balance + amount)
    }

    func testWithdraw() {
        let balance = 10000
        let amount1 = 1000
        let amount2 = 10001
        let cashier = Cashier(balance: balance)

        XCTAssertEqual(try! cashier.withdraw(amount: amount1), balance - amount1)
        XCTAssertEqual(cashier.getBalance(), balance - amount1)

        cashier.deposit(amount: amount1)
        XCTAssertThrowsError(try cashier.withdraw(amount: amount2)) { error in
            if let e = error as? CashierError, case .insufficientFundsError(let shortage) = e {
                XCTAssertEqual(shortage, amount2 - balance)
            } else {
                XCTAssert(false)
            }
        }
    }
}
```

冒頭部分の「@testable import ios_travis」がポイントです．普段アプリのコードを書いているTargetとテストコードを書くTargetが異なるため，明示的に**import**する必要があります．加えて，**@testable**という指定をしてやることで，元のクラスの可視性が**fileprivate**以下でない限り(すなわち，**internal**か**public**か**open**である限り)，可視性が**open**に上書きされ，参照できるようになり，さらに継承できるようになります．

テストは<kbd><kbd>⌘</kbd>+<kbd>U</kbd></kbd>で実行できます．


### 2.2 UI Tests
UI Testの例として，今回は次のようなVCを用意しました．先程のCashierを使用したものです．

```Swift:MainViewController.swift
import UIKit

final class MainViewController: UIViewController {
    @IBOutlet private weak var balanceLabel: UILabel!
    @IBOutlet private weak var amountTextField: UITextField!

    private var cashier: Cashier = Cashier(balance: 10000)
    private var histories: [History] = []

    override func viewDidLoad() {
        super.viewDidLoad()

        self.balanceLabel.text = String(self.cashier.getBalance())
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)

        self.amountTextField.resignFirstResponder()
    }

    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if segue.identifier == "showHistorySegue" {
            log.debug("showHistorySegue")

            if let vc = segue.destination as? HistoryViewController {
                vc.configure(histories: self.histories)
            }
        }
    }

    @IBAction private func didTapDepositButton(_ sender: Any) {
        guard let text = self.amountTextField.text, let amount = Int(text) else {
            return
        }

        let balance = self.cashier.deposit(amount: amount)
        self.balanceLabel.text    = String(balance)
        self.amountTextField.text = ""

        self.histories.append(History.deposit(amount))
    }

    @IBAction private func didTapWithdrawButton(_ sender: Any) {
        guard let text = self.amountTextField.text, let amount = Int(text) else {
            return
        }

        var balance = 0
        do {
            balance = try self.cashier.withdraw(amount: amount)
        } catch CashierError.insufficientFundsError(let shortage) {
            log.debug("AlertViewController will be shown")

            let alertVC = UIAlertController(title: "Error",
                                            message: "Amount(\(shortage)) is larger than current balance.",
                                            preferredStyle: .alert)
            alertVC.view.accessibilityIdentifier = "alert"
            let okButton: UIAlertAction = UIAlertAction(title: "OK", style: .default) { [weak self] action in
                self?.amountTextField.text = ""
            }
            alertVC.addAction(okButton)
            self.present(alertVC, animated: true, completion: nil)
            return
        } catch {
            fatalError("Uncaught exception was thrown")
        }

        self.balanceLabel.text    = String(balance)
        self.amountTextField.text = ""

        self.histories.append(History.withdraw(amount))
    }
}
```

いくつかポイントがあって，UI TestではUIButtonやUILabel等の部品を参照しやすくするために，**accessibilityIdentifier**という識別子を設定してやる必要があります．

コード中でなら`testButton.accessibilityIdentifier = "testButton"`という風にし，Storyboardでなら**Identity Inspector**を開いて下の画像のようにします．

![identifier.png](https://qiita-image-store.s3.amazonaws.com/0/35230/09046695-4b4a-9410-51ef-5398473549b3.png "identifier.png")


一応，これをしなくてもできないことはないのですが，確実性を上げるためにしておいたほうが良いです．

UI Testのコードはこんな感じになりました．

```Swift:MainViewControllerUITests.swift
import XCTest

class MainViewControllerUITests: XCTestCase {
        
    override func setUp() {
        super.setUp()
        
        // Put setup code here. This method is called before the invocation of each test method in the class.
        
        // In UI tests it is usually best to stop immediately when a failure occurs.
        continueAfterFailure = false
        // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method.
        XCUIApplication().launch()

        // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
    }
    
    override func tearDown() {
        // Put teardown code here. This method is called after the invocation of each test method in the class.
        super.tearDown()
    }
    
    func testDeposit() {
        let app = XCUIApplication()
        let textField = app.textFields["amountTextField"]
        textField.tap()
        textField.typeText("100")

        let depositButton = app.buttons["depositButton"]
        depositButton.tap()

        let balanceLabel = app.staticTexts["balanceLabel"]

        XCTAssertEqual(balanceLabel.label, "10100")
    }

    func testWithdraw() {
        let app = XCUIApplication()
        let textField = app.textFields["amountTextField"]
        textField.tap()
        textField.typeText("100")

        let withdrawButton = app.buttons["withdrawButton"]
        withdrawButton.tap()

        let balanceLabel = app.staticTexts["balanceLabel"]

        XCTAssertEqual(balanceLabel.label, "9900")
    }

    func testWithdraw_WithAlert() {
        let app = XCUIApplication()
        let textField = app.textFields["amountTextField"]
        textField.tap()
        textField.typeText("10001")

        let withdrawButton = app.buttons["withdrawButton"]
        withdrawButton.tap()

        let alert = app.alerts["alert"]
        XCTAssertTrue(alert.exists)
        alert.buttons["OK"].tap()
        XCTAssertFalse(alert.exists)
    }

    func testHistory() {
        let app = XCUIApplication()
        let historyButton = app.navigationBars["Main"].buttons["History"]
        historyButton.tap()

        let navigationBar = app.navigationBars["History"]
        XCTAssertTrue(navigationBar.exists)
    }
}
```

こちらはUnit Testのときのように「@testable import ios_travis」とする必要がありません．というのも，importしたところでデフォルトではVC等を参照できないからです．[こちらのIssue](https://github.com/Quick/Quick/issues/415#issuecomment-153885307)にあるように，実際のUIのテストはアプリが走っているプロセスとは分離されたプロセスで走っているようで，直接そのコードのアクセスできないようです．そのため，前述の通り，**accessibilityIdentifier**に設定された識別子等を頼りにランタイムにUI要素へアクセスしているっぽいです(詳しいことは未検証です)．

**accessibilityIdentifier**を用いたUI要素へのアクセス方法ですが，コードにある通り，テキストフィールドなら`app.textFields["amountTextField"]`，ボタンなら`app.buttons["withdrawButton"]`という風にします．

「いちいちaccessibilityIdentifierとか打っていられないし，UITableViewとかはどうやってアクセスするんだ💢」となるのも時間の問題ですが，その辺を全部解消してくれる便利な機能がXcodeにはきちんとあります．

UI Testのコードを追加したい箇所にカーソルを置いて，**Debug area**の上にある赤丸の**Record UI Test**ボタンを押すと，Simulator or 実機でアプリが起動して，自分が操作した通りのコードを自動で生成してくれます．

![ui_start.png](https://qiita-image-store.s3.amazonaws.com/0/35230/bed59d54-bf7b-7ff0-563c-2e3d09bb261c.png "ui_start.png")

![ui_record.gif](https://qiita-image-store.s3.amazonaws.com/0/35230/7f6e98ef-9448-8747-00a9-538c9b0eab5c.gif "ui_record.gif")

そのままでは機械的なコードなので，あとは適宜リファクタすればかなり楽できると思います．結構面白い機能で，これだけで小1時間は遊べます．


## 3. Travis CI
### 3.1 リポジトリの連携
いよいよ[Travis CI][travis]と連携していきます．過去にそれについて書いた記事があるので，そちらを参照して頂ければと思います．

* [Elixir × PhoenixのプロジェクトをTravis CIでテストする - Qiita](http://qiita.com/KAGE_MIKU/items/024c3a099fb965093b0f)


### 3.2 ビルド設定ファイル
肝心の`.travis.yml`の中身はこんな感じです．

```yaml:.travis.yml
language: objective-c
matrix:
    include:
        - osx_image: xcode8.1

before_install:
    - brew update
    - brew install carthage
    - carthage bootstrap --no-use-binaries --platform ios

script:
    - xcodebuild -scheme ios-travis -sdk iphonesimulator -destination 'platform=iOS Simulator,OS=10.1,name=iPhone 7' -configuration Debug test | xcpretty -c
```

**Carthage**を用いている場合，before_installのところに`carthage bootstrap --platform ios`を書くのを忘れないようにしてください(--no-use-binaries等のオプションは適宜プロジェクトに合わせて変えてください)．その際，**Cartfile.resolved**もgit管理下に置くのを忘れずに．

また，scriptのところにあるパイプで繋いだxcprettyですが，これは別に必須ではないです．`xcodebuild`のログを整形・省略してくれる便利なコマンドで，[Travis][travis]上で見やすくするのと，ログがあまりに長いとbuildがfailedになってしまうらしいとのことで自分は入れています．

詳しい導入方法については次の記事を参考にしてください．

* [Travis CIでxcprettyを使ってxcodebuildのログをきれいにする - Qiita](http://qiita.com/radioboo/items/bb3bba734a68f0c25c81)

具体的には，次のような感じです．

1. `bundle init`
2. **Gemfile**の編集
3. `bundle install`
4. できた**Gemfile.lock**と**Gemfile**をgit管理下に

```Ruby:Gemfile
# frozen_string_literal: true
source "https://rubygems.org"

gem "xcpretty"

```

### 3.3 Schemeの設定変更
最後に，[Travis][travis]がXcodeプロジェクトをビルドできるように，ビルドしたいSchemeをSharedにします．

![manage_sceme.png](https://qiita-image-store.s3.amazonaws.com/0/35230/0460213a-bb86-1dbe-9fe3-826b47d8d634.png "manage_sceme.png")

![scheme.png](https://qiita-image-store.s3.amazonaws.com/0/35230/9af83f26-4dd5-b5e4-14d7-1ce1a82f4649.png "scheme.png")


## 4. Codecov
### 4.1 リポジトリの連携
[Codecov][codecov]へアクセスし，「Sign up with GitHub」をクリックしてGitHubアカウントと連携し，その後，テストカバレッジを収集したいリポジトリを選択します．

![codecov.png](https://qiita-image-store.s3.amazonaws.com/0/35230/1a176b12-5329-d694-0380-a2b3e5f951d8.png "codecov.png")

### 4.2 ビルド設定ファイルの編集
そして，先程の`.travis.yml`ファイルの`script`の最後に`bash <(curl -s https://codecov.io/bash)`を追加します．最終的な`.travis.yml`ファイルは次のようになります．

```Yaml:.travis.yml
language: objective-c
matrix:
    include:
        - osx_image: xcode8.1

before_install:
    - brew update
    - brew install carthage
    - carthage bootstrap --no-use-binaries --platform ios

script:
    - xcodebuild -scheme ios-travis -sdk iphonesimulator -destination 'platform=iOS Simulator,OS=10.1,name=iPhone 7' -configuration Debug test | xcpretty -c
    - bash <(curl -s https://codecov.io/bash)
```

以上で[Travis][travis]と[Codecov][codecov]との連携は終了しました．`git push origin master`!!!


# 確認
[Codecov][codecov]のリポジトリページを見てみると，コミットごとのカバレッジの推移グラフやファイル・ディレクトリごとのカバレッジをかなり見やすく確認することができます．

![coverage.png](https://qiita-image-store.s3.amazonaws.com/0/35230/41a71409-a1ea-2e60-8c31-5807452e69a7.png "coverage.png")


![files.png](https://qiita-image-store.s3.amazonaws.com/0/35230/7eaada82-f89d-1fa9-a033-db0cb523b851.png "files.png")

さいこう．


# Badges
**BadgeをREADME.mdに追加するまでがCIです**

* [Travis][travis]

![travis_badge.png](https://qiita-image-store.s3.amazonaws.com/0/35230/e62e66d5-d4f4-a766-ca58-ddedb3981d50.png "travis_badge.png")

* [Codecov][codecov]

![code_badge.png](https://qiita-image-store.s3.amazonaws.com/0/35230/1104e45f-31b2-db22-a152-3b8cf12e8798.png "code_badge.png")

* [GitHub][github]

![readme.png](https://qiita-image-store.s3.amazonaws.com/0/35230/479db4e7-5a84-d398-d0ca-0bd127ad69dc.png "readme.png")

お疲れ様でした🎉🎉🎉🎉🎉


# 終わりに
次はipaの配布とSlackへの通知あたりまでしたいです．



# 参考サイト
* [iOSアプリにも継続的インテグレーション環境を - Developers.IO](http://dev.classmethod.jp/smartphone/iphone/ios-8-travisci/)
* [Travis CIでつくるiOSのCI環境（テスト・アップロード編） - Qiita](http://qiita.com/akaimo/items/9612041c50b1898dedc0)
* [Travis CIでxcprettyを使ってxcodebuildのログをきれいにする - Qiita](http://qiita.com/radioboo/items/bb3bba734a68f0c25c81)
* [Travis CI を使って GitHub 上の Swift プロジェクトをテストする - EZ-NET](https://ez-net.jp/article/96/6hS5SChD/S4sD0jPmTLoJ/)
* [codecov/example-swift - GitHub](https://github.com/codecov/example-swift)
* [GitHub Issue](https://github.com/Quick/Quick/issues/415)

[travis]:https://travis-ci.org/
[codecov]:https://codecov.io/
[coveralls]:https://coveralls.io/
[github]:https://github.com/