やらなイカ?

たぶん、iOS/Androidアプリの開発・テスト関係。

try! Swift Tokyo 2017 テスト系セッションまとめ #tryswiftconf

昨年に続き、3月2〜3日に開催されたtry! Swift Tokyo 2017に行ってきました。

テスト系のセッションが3つあったので、それらについてまとめます。

今年は海外からを含め700人を越える参加者があり、会場になったベルサール新宿セントラルパークの広いホールもこんな感じ(会場の2/3あたり後方から撮影)。

クックパッドアプリのテストを味わう - Tasting tests at Cookpad

初日、クックパッドの松尾さん(@Kazu_cocoa)の講演。全て英語でのプレゼンでした。すごい。

www.slideshare.net

このセッションでは、UIのテストについて、UIのテストがクックパッドの開発をどうサポートしているかについて語られました。スライドには"Tests"とだけ書かれていますが、当然ながら自動化されたテストのこと。

クックパッドアプリ(日本版)は5年間メンテナンス・アップデートを繰り返しており、UIの変更も大小あった。2014年(3年前)からUIテストの実装を進めている。

なぜUIテストを実装したか。書籍『Re-Engineering Legacy Software』には、「リファクタリングの前にユニットテストを書くことは、不可能であったり、無意味なものしか書けない」と書かれています*1

そこで取った基本戦略は、"中から"と"外から"。まず"外から"「UIテスト」でエンハンスバグ(デグレード)を防いで「リライト/リファクタリング」を行なう。そして"中から"「ユニットテスト」を書く。また戻ってUIテスト……と回していくことで、検証可能なコードを育てていく。

テストピラミッド*2では、ユニットテストが最も多く、UIテストは少ないことが理想とされる(このピラミッドに手動テストは入っていない)。これを逆にすると、手動テストの時間がとても大きくなり、開発サイクルは遅くなる。

iOSバージョンとデバイスiPhone/iPad/iPad Pro)の組み合わせテストも数パターン実施。実装戦略として、UI操作の80%をカバーしている。

UIテストのアーキテクチャは、ユーザシナリオにフォーカスし、シナリオをRubyで実装、これをAppium*3で実行している。シナリオ記述にはTurnip*4を用い、データ駆動テスト*5を書いている。

内部コードへの依存を減らす工夫。find_elementでUI部品を探すとき、xpathで記述してしまうとViewのヒエラルキーに強く依存してしまい、Viewヒエラルキーが変わる修正でテストが壊れてしまう。UI部品にはaccessibility_idを設定し、そのidで指定することで回避できる。

UIテストでは全ての境界値テストやバリデーションなどを実施しようとしないこと。UIテストの実行には時間がかかるので、この手のテストはユニットテストで実施すべき。

このような試みは、開発者の空き時間で実現できるものではないことを忘れないでほしい。

Q&A

  • Appiumを使っている理由はなにか。実行に時間がかかるが。 -> アプリの状態をクリーンにして、システムアラート(カメラの権限とか)を毎回出すことができる。XCUITestだとシステムアラートが1回出るともう出ないので。時間がかかるのは確かに問題だが*6
  • 時間がかかるので境界値やバリデーションを実行すべきでないとあったが、代替手段は? -> ユニットテストで実施するべき。

所感

非常に説得力のあるセッションでした。どうしてもUIテストはUI変更によって無駄になることも多く、忌避しがちでした。またXcode 7からXCUITestが追加されたことにより、統合テスト(Integration Testing)レベルでのUIテストが実装しやすくなりましたが、やはり限界はあります。

本セッションで語られた、UIテストをAppium+TurnipによるシナリオBDDで行なう、という手段は、現時点での最適解だと思うので、自分でも前向きに考えていきたい。

参考

引用されていた『Re-Engineering Legacy Software』の翻訳本

少し古いですが、テスト自動化研究会で書いたAppiumの記事*7 www.atmarkit.co.jp

@niwatakoさんの聞き起こし niwatako.hatenablog.jp

テスト可能なコードを書くということの2つの側面 - The Two Sides of Writing Testable Code

ここから二日目。KickstarterのBrandon Williamsさん(@mbrandonw)のセッション。日本語タイトルは「テスト可能なコードを書くための2つの視点」とかがよかったかも。テスト対象の入力と出力という2つの面にフォーカスした話。

なぜテストを書くのか。試練として、テスタブルなコードだけを書くようにしている。実装のための実装ではなく、ドキュメントとしての価値がテストにはある。

例えば、ファイルから読んだ数値を演算した結果をコンソールに出力する関数を考えてみる。実装は容易だが、テストでは以下の要因を考慮する必要がある。

  • ファイルの中身(実際のデータ)だけでなく、HDDの現在の状態(ファイルを読めるかどうか)、グローバル変数でファイルパスを指定など、これら隠されたインプットに依存する。
  • 関数の戻り値として正しい演算結果を返しているかは、コンソールだけ見ていてもわからない。アウトプットの評価方法も考え直す必要がある。

まずアウトプット。アウトプットの評価を難しくしているのは、副作用(side effects)。どう副作用をハンドルするか。副作用をテストターゲットの境界に持っていく。例えば、戻り値をタプルにして、演算結果とコンソールに出力するメッセージを返すように修正する。

次にインプット。インプットを難しくしているのは"Co-effects"*8だという考え方。これは、関数の外部にあって、実行結果に影響を与えるもの。それがないと実行できないもの。DI(Dependency Injection/依存性注入)と呼ぶ人もいる。

Co-effectsをハンドルするベターな手段は、これらをひとつのStructに入れてしまうこと。サービス、Cookieストレージ、ユーザ、DateProtocol.Type、Language、UserDefaultなど、20以上にのぼる。DateはProtocolを介して扱うことで、テストではテストダブル(モック)で固定の日時を与えることができる。

このようにリファクタリングすることで、簡潔にテストコードを記述できるようになる。

Q&A

  • テストを書くのは実装の後か、前か。 -> テスト駆動で開発している
  • 環境について、ReaderモナドやStateモナドを使っているか。 -> Gap environmentはコモナド(Comonad)になっている。プロパティベースではやっていない。

Q&Aルーム

セッション後のQ&AルームでBrandonさんに聞いたところ、Co-effectsとは2014年にTomas Petricek氏が提唱した概念で、以下の論文で述べられたもの。("xUnit Test Patterns"で述べられている)テストフィクスチャ(Test fixture)に近いが、コモナド、副作用を扱う考え方。

ついでに他の方の質問もメモした範囲で。

  • DateをJSTで扱っているが、CI as a ServiceではUTCなのでどうしたらいいか。 -> タイムゾーンは考えないでシンプルにテストすればいい。
  • テストが無い状態からテスタブルに変えていきたい。 -> まずSingleViewControllerをターゲットにする。UI操作ひとつひとつに分解して、操作(ボタンのタップとか)とその副作用(ボタンがグレーになるとか)を検証する。操作や副作用は個別の関数に分解しておき、その組み合わせを実装するようにする。Kickstarterのアプリがそうなっているので参考にするといい。
  • UIテストフレームワークは使っているか。 -> フレームワークは使っていないが、ios-snapshot-test-caseは使っている。あらゆる言語でテスト実行してスクリーンショットを撮っている。

所感

テストにおいて、入力と出力を捉えるというのは基本なのですが、それを改めてシンプルに解説されたセッションでした。

Co-effectsははじめて聞いた言葉で、有用ではあると思うけど煩雑さとのトレードオフはありそう、というのが現時点の感想。近々、上記の論文などを読んでみて改めて何か書こうと思います。

参考

KickstarteriOSアプリはオープンソース。本セッションのサンプルコードとして読むとよさそう。 github.com

@niwatakoさんの聞き起こし niwatako.hatenablog.jp

モックオブジェクトをより便利にする - Making Mock Objects More Useful

Jon Reidさん(@qcoding)のセッション。

なぜモックを使うのか。例として、レストランにおいて、テーブルの客、ウェイター、コックがいたとき、ウェイターが正しく注文を処理したことをテストするのに、毎回本物のコックが料理していては時間や食材などのリソースが浪費される。これを避けるため、ユニットテストでは偽の(Fake)コックを使う。

Swiftでは、コックはCookProtocolとしてあらわし、RealCook、Fake(Mock)Cookがそれを実装する形。CookProtocolは、関数cookRamen(bowls: Int, soup: RamenSoup, extras: [String])を持つ。 ウェイターは、イニシャライザでCookProtocolを受け取る。関数order()を持ち、この中でCookProtocol#cookRamen()を呼び出している。

テストコード(WaiterTests#testOrder_ShouldCookRamen())では、コックのモック(MockCook)をウェイターに与え、order()を呼び、ウェイターが内部で正しくコックのcookRamen()を呼び出しているかをテストしたい。

以下、実現手順を簡潔に。

  1. cookRamen()を呼んだことをテストする。MockCookでハンドリングし、モックのメンバ変数に保存するが、bool型で「呼ばれたこと」を表現するのでなく、int型で呼ばれた回数をカウントするべき。order()呼び出し後、XCTAssertEqual()でカウンタが1であることを確認する。
  2. cookRamen()のパラメタをテストする。例えばbowlsに渡された値をモックのメンバ変数に保存し、order()呼び出し後に確認する。値は最後に呼び出されたときの値しか保存できないが、テストに使うにはこれで十分。ただし保存する変数名はcookRamenLastBowlsのように"最後"を表現する名前にすべき。そして同様にorder()呼び出し後、XCTAssertEqual()で値を確認する。
  3. テストメソッド1つにAssertは1つにすべき、という原則がある。そのため、MockCookにverifyCookRamen()というヘルパーメソッドを作り、その中にAssert文を内包する。テストコードからは、verifyCookRamen()を呼ぶ。
  4. テスト失敗時のメッセージがverifyCookRamen()から出力されるためわかりにくくなるので、#file#lineをAssertの引数で渡すようにする
  5. どのAssertで失敗したかを示すため、Assertの第三引数にメッセージを渡すようにする
  6. パラメタextrasは配列だが、格納順序が違ってもテストはパスさせたい(壊れやすいテストを避ける)。クロージャでMatcherを定義し、配列の完全一致でなく、要素が含まれていればパスするようにする。
  7. さらにHamcrest Matchersを使うことで、配列要素の一部が異なる場合にわかりやすいメッセージを表示できるようにする

テストコードはガラスのような壊れやすい(fragile)ものではなく、竹のようにしなやかで柔軟性の高いものを目指すべき。

Q&Aルーム

少し立ち話的に、有望に思うSwiftのモックフレームワークはあるかと聞いてみましたが、やはり無いとの答えでした。

所感

内容は以前Jonさんのブログで読んでいた内容ですが、手順を追って簡潔にまとめられており、分かりやすかったのではないでしょうか。

また、通常、モックを使うことがテストを壊れやすいものにするという批判がある中、壊れにくいモックを書こうという方向性、そして最後の「竹のようにしなやかな」という例え*9もよかったのでいつか真似しよう思いました。

例えといえば、過去にJonさんはテストダブル(モックやスタブの総称)を"Stunt double"、つまりアクション映画の吹き替え(スタント)と表現していて*10、いつか真似しようと思っていたところ。

なお、Jonさんは子供の頃に三鷹に住まわれていたそうで、久々かつ子供時代に話す範囲の会話だけ、と言いつつ、日本語できる方でした。正直、このカンファレンスで一番のサプライズ。 そして今回、ブログ以前から購読してますよ、と直接お伝えできたのはよかった。

参考

本セッションのスライドとサンプルコード qualitycoding.org

上記ページでおすすめとされている"Refactoring"の翻訳本

今回のセッションの元ネタにあたるブログ記事 qualitycoding.org

モック系ではこれもおすすめ qualitycoding.org

@niwatakoさんの聞き起こし niwatako.hatenablog.jp

カンファレンス全体を通して

昨年に引き続き、とても居心地の良いカンファレンスでした。主催者、登壇者、スポンサー、スタッフ、そのほか関係者の方々、ありがとうございました。

昨年同様ですが、ルームを分けずに1スレッド進行、そのぶん各セッションは短め(最長でも40min?)なのは非常に良かったと思います。エッセンスを絞ったセッション+詳しく聞きたい人向けにQ&Aルーム(ここにも通訳付き!)という構成はかなり良かったです。

Q&Aルームが休憩所っぽくなっていた感もありましたが、まあそれはそれで。

あと、Swift自体を扱うセッションが少ないという話もあり、確かにそう思いはしたのですが、Swiftに縛られないことで面白いセッションが増えるのであれば歓迎したいです。

来年も予定されているとのことで、期待して待ちます。

*1:前提として「リファクタリングの前にテストを書いて振る舞いを保護しろ」と言われているが、ユニットテスト書けないこと多いよね?という話

*2:アジャイル開発まわりでしばしば使われる、テスト自動化のピラミッド

*3:Seleniumのモバイル版として知られるテスティングフレームワーク

*4:AppiumでCalabashのようなシナリオBDDを実現するツール。テストシナリオをGherkinという書式で記述できる

*5:Gherkinではシナリオアウトラインと呼ばれる記法で、シナリオ内に変数を置いて複数種類のテストを簡潔に記述できる

*6:実行を並列化しているような話をしていたような? 聞き落とし

*7:連載を止めているのは私です。本当に申し訳ない。次回はまさにAppium+Turnipの予定だったり……。

*8:同時通訳では「共作用」と訳されていましたが、少し違和感あったので原語のままとしました。慎重に考えていきたい。

*9:ブルース・リー死亡遊戯』のダン・イノサント戦を思い出しました

*10:日本だと影武者とかと表現することが多い印象