try! Swift の宿題: もっとテストを

Avatar tnantoka 2016-03-22

こんにちは、@tnantokaです。
前回に引き続き、try! Swiftカンファレンス関連のお話です。

テストを書こう!

みなさん、テスト書いてますか?
カンファレンス当日、Twitterでこんなアンケートがありましたね。
僕は仕事ではRailsを書くことが多く、その文化の影響もあり、テストは毎日書いています。が、iOSではRailsほど書けていません。
try! Swiftに参加して、何かコミュニティに貢献できることはないかと考えていたんですが、Rubyのテストを書くのが当たり前という文化を広めるお手伝いならできるんじゃないかな、なんて偉そうなことを考えつきました。
( 書き方には自信ないのであくまで文化を…)
というわけで、定期的にテストの話題も取り扱っていこうと思います。
手始めに今回は、前回作ったAppBoardというアプリにテストを追加してみます。

なぜテストを書くのか?

と、その前になぜテストなんて書かなきゃならないのか?
僕自身、テストを書くのはあまり好きではありません。(なのでTDDはモチベーションが続かずあまりやれていません。)
僕が一番テストに感謝するのは、リファクタリングの時です。
テストが書いてあれば、ちょっと前に自分が書いてしまった残念なコードを修正したり、新しく学んだ書き方を試したり、といったことが気軽にできるようになります。
変更するたびにたくさんの手動テストが必要だとリファクタリングも躊躇してしまいますよね。
では改めて、手を動かしていきます。

テストターゲットの追加

まずは、XcodeでCommand + Uを実行してみましょう。
何も起こらない場合、テストターゲットがないと思われますので、以下の2つのターゲットを追加します。
(AppBoardはこの記事を書きたかったのであえて、プロジェクト作成時にテストターゲットを外しました^^)
  • iOS -> Test -> iOS Unit Testing Bundle
  • iOS -> Test -> iOS UI Testing Bundle
Carthageで入れたフレーム枠は、Unit Testingのターゲットにも追加しておきます。UI Testの方には追加不要です。
参考: https://github.com/Carthage/Carthage#adding-frameworks-to-unit-tests-or-a-framework
これで、Command + Uでテストが実行されるようになるはずです。

最初の一歩

それでは最初のテストを書きます。
BoardにAppを追加した後、AppがちゃんとそのBoardに紐付いているか見てみましょう。
Railsだとこのレベルのは書かないですが、まだ自分のRealm力が不安なので…。
Unit Test Case ClassからBoardTests.swiftを追加して、以下のメソッドを追加します。
冒頭に@testable import AppBoardも書きます。
func testApps() {
    let account = Account()
    let board = Board()
    let app = App()
    let name = "App Name"
    app.name = name
    account.addNewItem(board)
    board.addNewItem(app)
    XCTAssertEqual(board.apps.count, 1)
    XCTAssertEqual(board.all.first?.name, name)
}
Boardのアプリが1件になることと、その名前がApp Nameであることを確認しています。
Command + Uで実行して問題なければ、「Appを追加したのに、どのBoardにも紐付いてなくて行方不明だ…」というバグがないことを確認できます。
UI Testも追加してみます。
これはAppBoardUITests.swiftに直接メソッドを追加しちゃいましょう。
func testCreateBoard() {
    let app = XCUIApplication()
    let name = "Board Name"
    let count = app.cells.count + 1
    app.navigationBars.buttons["Add"].tap()
    let alert = app.alerts["Add New Board"]
    alert.textFields.elementBoundByIndex(0).typeText(name)
    alert.buttons["Add"].tap()
    XCTAssertEqual(app.cells.count, count)
    let cell = app.cells.staticTexts[name]
    XCTAssert(cell.exists)
}
NavigationBarのAdd (+)をタップして、Board NameというBoardを追加。
その名前が表示されたCellが存在することを確認しています。
Command + Uで実行すると、以下のようにシミュレータが勝手に操作されて少し楽しいです。

CI

せっかく書いたテストです。CI上で動かしてみましょう。
まずは、https://travis-ci.org/ からGitHubのRepositoryを追加します。
あとは、Xcodeで以下の設定をします。
- Manage SchemesでSchemeをShared
- Edit SchemeのTestでGather coverage dataをONに
そして、以下のような.travis.ymlを追加してPushすればTravis CI上でテストされるようになります。
language: objective-c
osx_image: xcode7.2
before_install:
  - carthage bootstrap --platform iOS --no-use-binaries --verbose
script:
  - set -o pipefail && xcodebuild test -project AppBoard.xcodeproj -scheme AppBoard -sdk iphonesimulator -destination "platform=iOS Simulator,name=iPhone 6s,OS=9.2" | xcpretty
after_success:
  - bash <(curl -s https://codecov.io/bash)
ついでに、以下も設定しました。
こうすると、こんな感じでひとりぼっちの開発でも雰囲気が出ます。
開発者向けサービスは、オープンソース向けは無料であることが多いので使わない手はありません。
READMEにBadgeを貼れますしね!

テストを追加していく

これでテストを書く環境は整いました。あとはどれぐらい書くのか?という問題です。
テストにかけられる体力は、プロジェクトによって変わってくるので絶対の正解はないと思いますが、参考までに僕の経験談を書いておきます。

Rails

完璧に全部書こうとした時期もありましたし、「とにかく大事なのは実際にユーザーが操作する部分だ!」と考えて、E2Eテストを書きまくった時期もありました。どちらも保守が大変でした…。
今はモデルとコントローラー層をほぼカバレッジ100%にする、という方針でやっていますが、1年以上うまく行っています。

iOS

モデルだけ書いているプロジェクトが多いですが、KIF等を使ってがっつりE2Eテストをやったものもありました。
今は、
- このアプリで絶対この機能は使えないとダメ1という部分はUI Testする。
- ModelとViewModelはできるだけカバレッジ100%に近づける。
という方針です。
いくつかテストを追加して、AppBoardのカバレッジはこんな感じになりました。

UITestでアプリの機能を一通り操作しているので、カバレッジがあがっていますが、ユニットテストはもっと追加したいところです。
またAPIがエラーになった時のテストが全くできていないため、実APIを叩くのをやめるのと共に対応したいです。

ソースコード

https://github.com/tnantoka/AppBoard/releases/tag/v1.0.1

関連セッション

参考情報

ハードウェアキーボードを接続していると、UI TestでSearchBarに入力ができませんでした。日本語キーボードも要注意です。
https://forums.xamarin.com/discussion/59518/unable-to-type-into-searchbar-on-uitest

  1. AppBoardでいえば、Boardを作ってAppを検索して追加・表示するところまでです。削除は動かなくても使うことはできます。