Swiftでの自動テスト

ソフトウェアのテストはプログラムの振る舞いを確認するためとコードの品質を改善するときに非常に良いツールとして働きます。今回の Jeff Hui の発表では、テストに関するツールや技術、Quick を使ってのテストの書き方などについて学ぶことができます。また、関数型プログラミングの世界では広く知られているテスト生成技術 QuickCheck についても触れられています。発表で使われているコードは GitHub にあります!


テストをする理由 (0:00)

なぜソフトウェアのテストを行わなければいけないのでしょうか? それには、たくさんの理由があります。ここでは簡単にまとめておくと、主な理由はソフトウェアの品質を向上させるためです。テストを書くことにより、変更を加えたときに他の部分にどのような影響を及ぼしたのかすぐにフィードバックが得られるようになります。これによりコードを変更するときに自信が増します。

ソフトウェアの品質を改善するのは明らかにテストを書くことだけではありません。もちろん、他にもたくさんの要素が影響してきます。そして、テストはそのうちの一つに過ぎません。また、テストを行うのにも “TDD(テスト駆動開発)” 、 “テストファースト”、他にもさまざまなテクニックや戦略、方法論があります。手動テストもありますが、ここではそれらについてはあまり詳しくは話さないことにします。

今回は “プログラムの意図を定義し振る舞いを繰り返し確認する自動化” についてお話しいたいます。
“Automated(自動化)” について説明すると、ショートカットキーを押すだけで、すべてのテストが実行され どの程度アプリが意図通りに動いているのか確認できるべきというものです。テストを実行する手順は、複雑なものであったり手動でする作業があるというわけではありません。
“Describe intent(意図の記述)” は非常に大事なことです。テストを書くことは、壊れている箇所を見つけるだけでなく、なぜそこが壊れているのか知る事が重要だからです。ソフトウェアには常に変更は加えられ続けます。そして、その要求は変わり続けます。テストを書くことは、なぜその部分が壊れているのか知るのにとても良く働きます。自動テストをする主な利点というのは、振る舞いが確認できることです。期待した動きをしているのかどうか確認をすることが大きな目的となります。
そして ”Repeatability(再現性)” については、ツールは、再現性を高める手助けをしてくれます。しかし、実行時にたまに落ちたりしないかやテストが再現性があるものかを確認することはテストを書く私たちの仕事です。そうでないと、意図しない動きが出てときに受け取るメッセージの信用が無くなるので、自動テストの多くの利点を失うことになります。また、どのような環境で実行してるかに関わらず、この再現性は保たれるべきです。

ツール

XCTest (4:24)

Apple はユニットテストをするツールとして XCTest を提供しています。使うのはとても簡単です。今は Xcode でプロジェクトを作成時にチェックボックスすらなく自動で入るので、全てのアプリにテストがあることになります。以下のコードは、XCTest をインポートし、基本的なテストを書いています。また setUp, tearDown では下準備や後始末の処理を書きます。

import XCTest

class TestSort : XCTestCase {
    var values: [Int] = []

    // Shared setup goes here
    override func setUp() {
        values = [2, 5, 3]
    }

    func testReorderingOfSmallerIntegersFirst() {
        // Performing an action
        sort(&values)

        // Assertion
        XCTAssertEqual(values, [2, 3, 5],
            "Expected \(values) to equal [2, 3, 5]")
    }
}

記事の更新情報を受け取る

どのようなテストをするか考え、XCTest を使ってテストを書き、プログラムの動きを確認します。XCTest はコマンドラインと XCode で自動でテストが行える素晴らしい手段を提供してくれています。アサーションを使って、振る舞いを確認していきます。期待する振る舞いを確認していき、おかしなことをしない限り何度も実行することが可能です。しかし、XCTest ではテストの目的をテストケースのメソッド名に書くぐらいしかできず本当の “意図の記述” ができません。

Quick (6:31)

XCTest は本当に良いツールだと思います。ですが、プログラムの意図についてそれほど説明できないため多くの開発���は Quick などの他のツールを使ったりします。Quick は BDD(ビヘイビア駆動開発) テストフレームワークで、大半が XCTest と同じです。しかし、それに加え、意図を記述することにフォーカスしており、そのコードをなぜテストしているのかより詳しく記述できるようになります。

以下のコードは Quick での同じ例です。見た感じはほとんど同じで少し長いぐらいです。”describe” が XCTest で言うテストクラスに当たり、”it” がそれぞれのテストケースとなります。また “beforeEach” が “setUp” です。”describe” は、好きなようにネストして書けます。これら API は “意図の記述” によりフォーカスしていると言えます。

import Quick
import Nimble

class SortSpec: QuickSpec {
    override func spec() {
        describe("sorting integers") {
            var values: [Int] = []

            beforeEach {
                values = [2, 5, 3]
            }

            it("reorders smaller integers first in the array") {
                sort(&values)
                expect(values).to(equal([2, 3, 5]))
            }
        }
    }
}

テスト

アプリケーション (8:40)

ここでお見せする “RandomApp” というデモアプリがあります。これは、乱数をインターネット経由で取得して表示するものです。このアプリは特に複雑なところはありません。それにデザインも特に何も考えられていません。デザインの大半の部分が、視覚的な効果でこれからテストする振る舞いとはそれほど関係ないからです。アプリは、”RandomClient” と “DetailViewController” があり、それらとやりとりをする “ListViewController” で構成されています。では、ここでどこにソースファイルを置くべきでしょうか? それは、プロダクションのコードはアプリのターゲットにテストコードはテストバンドルにあるべきです。こららのファイルは決して混在させてはいけません。テストバンドルでは、プライベート名前空間として見なされます。そのため、両方のターゲットで共通するクラスを作った場合、テストバンドルは内部表現のクラスとして参照するようになります。これは、非常にデバッグが困難な状況を作り出してしまうため、必ず別々にするよう注意してください。

ネットワーク (14:08)

Objective-C ですでにあるライブラリを利用すれば、ネットワークの部分をテストする方法はたくさんあります。ここでは “Nocilla”“OHHTTPStubs” について紹介します。これらは Ruby でいう “webmock” のような HTTP コードをテストする便利な機能を提供しています。素晴らしい API であり、テストの中でネットワークリクエストを全てスタブしてくれます。これによりテストはネット経由で通信を行わなくなります。これは非常に便利で価値のあることです。特にすでにあるアプリなどにテストを追加する場合は役に立ちます。Nocilla と OHHTTPStub はどちらとも “MethodSwizzling” を利用しています。これは Objective-C の機能で、ネット上にたくさんそれについての記事はあるので、ここではあまり話さないことにします。

アダプタ (15:11)

アダプタパターンは、多くの場面で使えます。ネットワークのテストに加え他の様々な部分のテストに役立ちます。”アダプタ” はわかりやすい言葉で言うととプロトコルです。以下のプロトコルはリクエストを受け取り結果をコールバックで返すメソッドを一つ持っています。プロダクションコードの中では、NSURLConnection を使って通信を行っています。

public protocol HTTPClient {
    func sendRequest(
    request: NSURLRequest,
    complete: (NSURLResponse?, NSData?, NSError?) -> Void)
}

public class URLConnectionHTTPClient: HTTPClient {
    let queue: NSOperationQueue

    public init() {
        queue = NSOperationQueue.mainQueue()
    }

    public func sendRequest(request: NSURLRequest,
        complete: (NSURLResponse?, NSData?, NSError?) -> Void) {
            NSURLConnection.sendAsynchronousRequest(request,
            queue: queue,
            completionHandler: complete)
        }
}

プロトコルを使う利点は、RandomClient のような場合、テストで別の偽物の Client クラスとすり替えることができます。正しいリクエストが送られているか、予期せぬ JSON データを受け取った時にどのような振る舞いをするかなどの確認が行えます。アプリでネットワークエラーが起きた時はどうなりますか? アダプターパターンをベースにこういったことが簡単に行えます。

また、アダプタパターンはたくさんのことに応用できます。アプリのコアロジックから分離して実装できるので非常に便利です。ネットワークやロギング、アナリティクス、永続化などで使えます。CoreData から Realm に乗り換えるときなど API 構造を揃えておくことができるので、このアダプタパターンは役に立つと思います。アナリティクスでもすぐに変えたがる人が多いので便利である思います。

UIKit (18:14)

RandomApp では UIKit を使ってテストを行っています。UIKit を使ってのテストは、たくさん UI の変更がある場合は、たしかにトレードオフの関係にあるものです。しかし、UIKIt にはテストを行う便利な機能がたくさんあります。そのうちの一つは View のライフサイクルの管理を行う ViewController です。 以下の二つのメソッドは、ライフサイクルを呼び、ナビゲーションをコントロールするパブリックな API です。前者は、”viewWillAppear” を暗に呼び、後者は、”viewDidAppear” を呼ぶことになります。第一引数を true から false に変えると、”disappear” の処理も行えます。普段は ViewController のライフサイクルのメソッドは Apple のフレームワーク内で処理されていることです。

viewController.beginAppearanceTransition(
    true, animated: false)
viewController.endAppearanceTransition()

同様に、他のコントロールの部分でもたくさん同じような API が使えます。以下のコードでは、デモアプリのナビゲーションにバーボタンがあるため “UIBarButtonItem” のタップを再現しています。Objective-C の用語で、ターゲットとセレクタを使って呼び出す “Target-Action” があります。現在、Swift では Objective-C のランタイムである “msgSend” を呼び出す機能は完全にはサポートされていません。今回は Objective-C の “performSelector:withObject” を呼び出すだけのクラスを作成しています。

// Swift
func tap(barButtonItem: UIBarButtonItem) {
    SelectorProxy(target: barButtonItem.target).
        performAction(barButtonItem.action,
        withObject: barButtonItem)
}

// Objective-C
void tap(UIBarButtonItem *barButtonItem) {
    id target = barButtonItem.target;
    SEL action = barButtonItem.action;
    [target performSelector:action withObject:barButtonItem];
}

おそらくテストを行うのが難しい部分は、よく使われることがあるであろう TableView と CollectionView です。これらはかなりの最適化が行われており、かなり奇妙な振る舞いをします。ほとんどの部分で C++ で書かれており、どのように描画されるのかについてのコンテキストを持っています。一番厄介な部分は、TableView と CollectionView のデリゲートメソッドです。デリゲートを通して、実際の View が適切に紐付いているのか確かめます。

tableView.selectRowAtIndexPath(
    indexPath,
    animated: false,
    scrollPosition: .Middle)

tableView.delegate?.tableView?(
    tableView,
    didSelectRowAtIndexPath: newIndexPath)

QuickCheck & Fox (23:20)

テストやコードの振る舞いを確認する方法にはいつもより興味深いやり方が存在します。たとえば、2,5,3 でテストを行っていますが、なぜこの3つの数字を選んだのでしょうか? 実際のところこれらは適当に選んだ数字ですが、すべて 5 を選ぶのとは何が違うのでしょうか? この場合は、ソートのテストを行っているので、全て同じ数字で行うと何もしないのと同じになります。それが、違う数字を入力データとして選んだ理由となります。

関数型コミュニティには入力のデータを自動で選んでくれるあるツールがあります。手動で特定のテストを書いていくのではなく、テストを生成してくれます。QuickCheck は、問題の箇所が十分わかっていない時や特定の入力のときの振る舞いがわかっていない場合などでもテスト生成を自動で行ってくれます。プロパティを定義し、全称記号を適用し、それに対してどのような振る舞いを期待するのか記述します。ここから、失敗するテストケースを見つけることが QuickCheck の仕事となります。

Fox という QuickCheck を Swift と Objective-C に移植したものを作りました。たくさんの入力データでテストを行い、失敗するケースを見つけるまで実行します。また、失敗が見つかると入力データで一番小さい失敗するケースを探してくれます。もちろん、なぜそれが失敗しているのか理解するのは開発者がやることです。なぜなら入力データを生成しその振る舞いを推測しているにすぎないからです。しかし、いろいろなデータで試しデバッグを簡単にする良い結果を教えてくれます。Fox は、よりデバッグをしやすくするための次のステップである入力データから何がシグナルでノイズなのかについて知らせてくれるのです。

状態 (29:01)

ある状態があるとき、それを値として表すことができますか? 以前の Andy Matuschak の話でもありましたが、値というのはパワーとレバレッジを与えてくれます。Fox ですることはステートマシンを使って値を表します。状態モデルを使って一つの状態を扱っていきます。

状態モデルとは、ユーザーの状態をアプリのメモリにどのようなデータがあるのかを表しています。Fox はデータベースのようなディスクに保存されているデータに関しては考慮しません。 メモリですべてのテストを実行しています。それにより実装はシンプルにできました。”RandomApp” について考えるとすると、通信をしている状態を表す “Active Request” と表示している状態を表す “Received Number” となります。

トランジションは抽象的なレベルでどのようにプログラムが振る舞うのかを表したものです。これを実際のアプリの場合に適用し Fox に伝えます。また、どのような行動、振る舞いを状態モデルで受け入れるかを基準とした前提条件があります。テストコードと実際のコードがどのようにやりとりをするのかについてのコードを書きます。必要であればアサーションも使用できます。このアプリだと、ネットワークリクエストが戻ってきたときに “Received Number” に戻っています。

参考 + Q&A (37:01)

Q: 結合テストについてはどうお考えですか。シュミレータで実行することについてどう思いますか? オススメできて、遅過ぎるということはないでしょうか?
Jeff: 私の意見は、結合テストはたくさんのポテンシャルと価値があり、かなり興味深いことです。しかし、現在まだプログラムの正しさを測る指標としては信頼できない気がします。確かに結合テストのテストツールを使えば iOS シュミレータを使ってなどして行えます。しかし、まだ信頼性に欠けています。ここでいう信頼性とは、バージョン間で壊れやすいという意味です。

Q: テスト生成を使った時のスピードはについてはどうですか?
Jeff: この部分については話していませんでしたが QuickCheck はセミランダムなテスト生成をします。どのように縮めていくかとどのようにデータを生成するかがあります。どのぐらいテストケースを生成するかのような設定可能な値がたくさんあります。

Q: QuickCheck は毎回テストケースを生成してくれるみたいですが、偏ったりしないのですか?
Jeff: はい、偏ることを防ぐために乱数を生成するシードがあります。同じテストを再現したい場合は、同じシードを使えばできます。

Q: 私はテストケースやテストクラスを個別に実行できるところが XCTest で気に入っているところです。Quick と XCode の統合はどうなっていますか?
Jeff: 残念ながら、Xcode と XCTest は離すことが不可能なぐらいくっついています。私は Objective-C 用の BDD フレームワークである cedar も開発しています。cedar では完全な Xcode の統合が実現されています。

Q: Fox には乱数生成器が入っているのですか?
Jeff: はい、それに加え、独自の型や値に対して行いたい場合などの生成されたデータも作成できます。Fox が提供しているものの上に自由にカスタマイズが可能です。

Q: Swift にはアクセス修飾子があり、テストを書くときには扱いにくいものだと思います。テストはモジュールの外側にあるので、実際のアプリのコードにアクセスできません。これについて何か解決策はありますか?
Jeff: 残念ながら、私も public とたくさん書いています。現在の Swift では仕方がないことだと思います。public を書く以外に解決する方法は知りません。

Q: Swift には nonnull や nullable があります。Fox は null や nonnull のテストが行えますか?
Jeff: API が nil を受け入れるか選択することで、テストすることが可能です。Fox に nil をデータセットの中に含めるなどの設定ができます。

Q: Swift では動的なディスパッチができなくなりました。Swift のテストでモッキングのようなことができますか?
Jeff: NSObject やそれ以外の Objective-C のクラスを継承しているクラスに対しては Objective-C のランタイムを使用しているのでモッキングができます。将来的にはできるようになるかもしれませんが、現在、純粋な Swift ではできません。そのため、今回アダプタパターンを紹介しました。アダプターパターンは動的な特性を持たない他の言語などでよく使われます。

Q: おそらく Objective-C が大半で Swift が少数のプロジェクトがまだほとんどだと思うのですが、そういった中でテストを書くことについて他にどういう考えを持っていますか? 他にも普段やられているテストのアプローチなどあれば教えていただきたいです
Jeff: もちろん Quick と XCTest は Objective-C で動きますので何も問題なくテストを行えます。多くの開発者が実装のコードとテストのコードを同じ言語にすることを好んでいると思います。型チェックがある言語は予期せぬ型が入り込むことを未然に防いでくれますので、その分ソフトウェアの品質が上がると思います。

Q: テスト自動生成のようなものを使った経験はないのですが、プロパティの部分についてもう少し詳しく教えていただきたいです。これから作りたいデータに対してどのように制約を加えていけば良いでしょうか?
Jeff: 他の型でも行うことはできます。Haskell の QuickCheck も知っているのですが、Haskell ではとても優れた型システムを持っているので型推論をしてくれます。他の言語でも、使用したい型を指定してあげれば良いだけです。
観客: Functional Programming in Swift という本があります。この本には Swift での QuickCheck の実装について書かれています。

Q: 実装コードよりもテストコードを書かなくするためにテストの自動生成を行いたいのですが入力データにどのように制約を課せばいいですか?
Jeff: 特定の仕様に関わらず、初めからすべての部分をカバーする必要はないと思います。テストする範囲を絞って小さなものから行い、時間をかけて複雑なものに発展させていきます。徐々に複雑なシナリオのテストを行っていきます。

Q: テスト生成をいつ止めると決まりますか? 10分間テストを生成し続けるような指定の仕方ができる、何かツールのようなものはありますか?
Jeff: ツールはありません。Fox ではそのような指定の仕方はできません。今は、プロパティ毎にどのくらいテストを生成するかという数字で決めています。デフォルトは200です。これはただの適当な数字です。10分間テストを走らせるのような指定の仕方ができるようなロードマップにはありますが、今はまだ実装されていません。

Q: XCTest の実行中に Exception が出ても止まらせない解決策を知っていますか?または、この問題を解決してる他のフレームワークを知っていますか?
Jeff: Quick も XCTest を基準としているので同じ問題がありました。しかし Apple がこれを解決する手段を提供してくれています。XCTest ケーステストにはテストケースが失敗した後に呼ばれるプロパティがあります。そのプロパティに false をセットすることでコントロールできます。

Q: Pivotal Lab でのテストの使われ方について質問があります。ユニットテストやテスト生成をどのように普段の業務では使われていますか? プロトタイプを行って、次にテストを書いてなど特定のワークフローはありますか?
Jeff: Pivotal Labs は私が今働いているコンサルティングの会社です。テスト駆動開発を熱心に取り組んでいます。また、大部分でユニットテストが書かれています。大抵、テストを書きながらソフトウェアの品質を上げていく方法でプロダクトを作っています。はじめにクライアントから何が作りたいのか、どうやって作りたいのかなどについての調査します。もし、まだ製作前のリサーチの段階であれば、テストは使いません。単にプロトタイプを作って少し見せるようなときには、テストを書く価値がないと思います。


Jeff Hui

Jeff Hui is an full-stack engineer specializing in iOS development. He’s worked on a number of iOS apps as a consultant. He’s an active open source contributor and the core team member to Quick & Nimble testing frameworks.