やらなイカ?

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

UWA GOTでUnityアプリのプロファイリング(iOS編)

Unity向けプロファイリングツールであるUWA GOTのv2.0.1から、iOSプラットフォームでのプロファイリングも可能になりました*1。 本記事では、iOS版の導入手順(Androidとの差分)と注意点などを紹介します。

なお、UWA GOTの概要については、過去記事を参照してください。

www.nowsprinting.com

www.nowsprinting.com

SDKインテグレーション

基本的なインテグレーションはAndroidと同様です。 ただし、最初に開くSceneのヒエラルキには、UWA/Prefabs/UWA_Launcherを追加します*2

以上設定したら、Development Buildを有効にしてビルドします(Xcodeプロジェクトを生成します)。

Bitcodeを無効化

XcodeプロジェクトはデフォルトでBitcodeが有効な設定になっていますが、UWA GOTのiOSライブラリはBitcodeに対応していません*3。 そのため、Build Settings > Project*4 > Build Options にある"Enable Bitcode"を探して"NO"に設定する必要があります(下図参照)。

f:id:nowsprinting:20200222224220p:plain

なお、ビルドごとに毎回手作業で修正するのはつらいので、Unityビルドの後処理でXcodeプロジェクトの"Enable Bitcode"を"NO"に書き換えるスクリプトを入れておくと便利です。

以下のコードをEditorディレクトリ下に配置します。このコードは、Unity 2019.3.2f1 + Xcode 11.1で動作を確認しています。

public class DisableBitcode : IPostprocessBuildWithReport
{
    public int callbackOrder => 0;

    public void OnPostprocessBuild(BuildReport report)
    {
        if (report.summary.platform == BuildTarget.iOS)
        {
            var path = report.summary.outputPath;
            var pbxProjectPath = PBXProject.GetPBXProjectPath(path);
            var replaceProject = new PBXProject();
            replaceProject.ReadFromString(File.ReadAllText(pbxProjectPath));

            var target = replaceProject.GetUnityFrameworkTargetGuid();
            replaceProject.SetBuildProperty(target, "ENABLE_BITCODE", "NO");

            File.WriteAllText(pbxProjectPath, replaceProject.WriteToString());
        }
    }
}

スクリプトは、下記ブログ記事を参考にさせていただきました*5

blog.naichilab.com

計測

UWA GOTをバンドルしたアプリを起動すると、下図のGUIがオーバーレイされます。

f:id:nowsprinting:20200222224757p:plain

"Direct Mode"は、onにするとアプリが終了し、再起動するところから計測を開始できます。offのときは、"Overview", "Assets"いずれかをタップしたタイミングで計測を開始します。

計測が開始されると"Stop"ボタンが表示されるので、一通り動作させたらこれをタップして計測を終了します。 一度計測を行ったら、アプリを再起動しなければ次の(他の)計測はできません*6

f:id:nowsprinting:20200222224804p:plain

計測データのアップロード

Android版と異なり、計測が終了するとデータをアップロードするためのダイアログが表示されます。

f:id:nowsprinting:20200222225228p:plain

UWAオンライン(IDとパスワード入力が必要)、もしくはローカルサーバ(IPアドレスの入力が必要*7)を選択し、[データ提出]をタップします。

Onlineの場合、スクリーンショットをUWAオンラインサーバに送信するかを指定するトグルが表示されます*8。[確認]をタップすると計測データが送信されます。

f:id:nowsprinting:20200222225236p:plain

計測データの表示

以降は、Android版と同様です(ただしMonoプロファイルはありません)。過去記事およびマニュアルを参照してください。

*1:本稿執筆時点ではv2.0.2がリリースされており、本稿はv2.0.2ベースで書いています

*2:v2.0.1まではプラットフォームごとに別ファイルでしたが、v2.0.2から統一されました

*3:UWAに修正要望を出しています

*4:ProjectでなくUnityFrameworkだけでよさそうですが、スクショ撮りなおすのが面倒だったのでここままで…

*5:ただしUnity 2019.3でPBXProjectの構成が変わっていたため修正

*6:改善要望はしており、修正予定はあるそうです

*7:Unityエディタ上でGOT Editorを開き、WIFIボタンをクリックすると起動します。IPアドレスとポート番号が表示されますがポート番号は入力不要です

*8:Direct Modeもそうなのですが、トグルon/offがとても視認しづらいです。要望は上げています…

システムテスト自動化カンファレンス2017-2を開催しました #stac2017

ヤフー株式会社さんで開催された、テスト自動化研究会の旗艦イベント「システムテスト自動化カンファレンス2017-2」にスタッフとして参加してきました。

testautomationresearch.connpass.com

通算5回目、年1回の開催ですが、昨年分が15月にずれ込んでしまったため「2017」と銘され、今回が「シン・2017」ということになります。まぎらわしくてすいません。

今回はチュートリアルのTAとしてお手伝いしたため余り講演は聞けなかったのですが、今回からはじめた公募セッションやLTが入ったことで、扱う幅が広がったように思え、有意義なカンファレンスであったように思います。

Magic Pod チュートリアル

チュートリアルはキャンセルも出て想定より少ない人数での開催でしたが、その分TAの目が届きやすくなり*1、ほとんどの方がInstagramアプリを題材にした実践課題*2まで完遂できたようで、正直ほっとしました。

内容は、モバイル向けテスト自動化ツールであるMagic Podの使いかたを紹介するもので、これはappium導入のハードルとなっている「操作対象のUI要素を指定するためのロケータの検出」を自動化し、また、テストケースを(テストコードを書くのでなく)ブラウザ上のマウス操作で作成・実行できるというツールです。

Magic Podは無料で使い始めることができますので、興味を持たれた方はぜひお試しください。

www.slideshare.net

講演資料

以下、拾えた範囲でスライドを。追って、テスト自動化研究会の"イベント&アーカイブ"のページにレポートが出るはずです。

テスト自動化と機械学習(STAR機械学習分科会紹介)

TODO: 公開されたら貼ります

翻訳者の同僚が語る「初めての自動テスト」

www.slideshare.net

9月に発売された書籍『初めての自動テスト』について、翻訳者の玉川さんが本日来られないとのことで、同僚である太田さんによる講演。

書籍はこちら。

初めての自動テスト ―Webシステムのための自動テスト基礎

初めての自動テスト ―Webシステムのための自動テスト基礎

GebとSpockではじめるシステムテスト自動化

www.slideshare.net

楽天のレジャー・サービスにおける自動化の取り組みとその効果

www.slideshare.net

How we automate E2E tests at Mercari

TODO: 公開されたら貼ります

TypeScript + PhantomJSを利用した効率的なテスト実施

speakerdeck.com

SI-Toolkitでテスト自動化を実現する現場で遭遇した出来事

www.slideshare.net

自動化困難な状況での活動方法

www.slideshare.net

LT

テスト自動化の前の1.5年にやったこと

楽天の荻野さんのLT。タイトルはうろ覚えですすいません。

レガシーなシステムにテスト自動化を導入したときの、スクリプトを書く前に実施した、開発基盤の整備やQAマネージャのレベルアップといった施策の紹介。それらを整える期間は目に見えた進捗は少ないが、「あとはスクリプトを書くだけ」になってから一気に自動化が進んだ、というお話。

IoT基盤を活用したテスト時間の短縮

折田さんのLT。自動テストを実行するためのPCをたくさん積むという話をよく聞くが、PCはテスト対象の動作待ちがメインなのでスペックは低めでよく、むしろ夜間に通電したまま放置できるものが望ましい、そこでRaspberry Piを使う、というお話。

スタッフ反省会で実機(名前がR2-D2とK-2SO!)を見せていただきました。次は動いてるところが見たい!

テストも開発もするモバイルエンジニアのためのXCUITest/Espressoのすすめ

speakerdeck.com

appiumのメリットは語られるけど、実際にはそれほど楽に享受できるわけではなく、それならばiOSはXCUITest、AndroidはEspressoを使うほうが便利、というお話。

ちなみに私は、開発者テストとしてのIntegration Testingであれば完全に同意。システムテストとして、End to Endとして、という話になると、やはりappiumなどでリリース向けのパッケージ(ipa/apk)をテストすべきかな、とは思います。割り切りですが。

フォローアップのエントリはこちら。

woshidan.hatenablog.com

gaugeによるe2eテスト

テスト自動化ツールGaugeの紹介

テストケース構造をモデル化しよう!:テストカタマリー紹介

www.slideshare.net

*1:逆に介入が多すぎたと感じた方がいたら申し訳ない

*2:ログイン・ログアウトだけでも色々な振る舞いがあって複雑なのです

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:日本だと影武者とかと表現することが多い印象

実践 Appium(書評らしきもの)

11/26に発売となる書籍『実践 Appium』をご恵贈いただき、一足先に拝読させていただきました。

実践 Appium

実践 Appium

『実践 Appium』は、洋書『Appium Essentials』(Packt Publishing, April 2015)の翻訳書です。Appiumのバージョンは原著執筆当時の1.5を前提に書かれており、サンプルコードも原著のものをそのまま使いますが、監訳者によって最新のAppium 1.6での動作も確認されているそうです。

なお、Appiumとは、iOS/Android向けの自動化ツールで、ネイティブアプリ、ハイブリッドアプリ、Webアプリ(Webブラウザ)向けの自動テストスクリプトPythonRubyといった任意の言語で記述・実行できるのが特徴です。 スクリプトは、Webアプリケーションのテスト自動化ツールの定番と言えるSeleniumと同じシンタックスで記述できます。

書かれていること

  • Appiumだけでなく、iOSアプリ、Androidアプリの自動テストを実行するための環境設定(SDKのインストール等)から丁寧に書かれています。開発者だけでなく、QA担当者が読むことを意識されている印象
  • Appiumサーバの設定について、各項目の解説が書かれている
  • iOS/Androidそれぞれの、ネイティブアプリ、ハイブリッドアプリ、Webアプリについて操作方法が書かれている
  • iOSシミュレータ/Androidエミュレータだけでなく、iOS/Android実機でのテスト実行方法も書かれている(iOSのプロビジョニングプロファイルまわりの記述も)
  • GUIの操作について、単純なタップだけでなく、スクロール、スワイプ、スクリーンショット、システムダイアログへの対応など、実際のテストを書くにあたって必要な操作が書かれている

書かれていないこと

基本的に、GUIベースでAppiumサーバを立ち上げ、テストスクリプトを実行する、という範囲のみ書かれています。これらをCLIで実行し、CIに組み込むあたりのノウハウは得られません。

また、日本語版の書き下ろしは無く、サンプルコードも原著のものをPACKTのサーバやGitHubからダウンロードして使います。すでに原著を読まれている方は書い直す必要はなさそうです。

本書をおすすめするポイント

Appiumを使い始める方、触ったことはあるが業務に本格的に導入しようとしている方には特におすすめできます。

Appiumに限らずオープンソースのツールは変化が早く、逆に言えば本書のような書籍の内容は陳腐化が早いと言えます。しかし、いざ着手しようとしたときに断片的な情報をネットで収集するのは骨が折れるものです。 すぐにはAppiumを使用しない、という方も、本書発売を機にAppiumの"現状"を知っておく価値はあるはずです。

関連書籍

Selenium実践入門 ―― 自動化による継続的なブラウザテスト (WEB+DB PRESS plus)

Selenium実践入門 ―― 自動化による継続的なブラウザテスト (WEB+DB PRESS plus)

Seleniumデザインパターン & ベストプラクティス

Seleniumデザインパターン & ベストプラクティス

関連してそうでしていないもの

かえる本(Jenkinsのほう)

Jenkins

Jenkins

かえりマン

#tryswiftconf 3日目のテスト系セッションまとめ

3/3〜5の3日間、サイバーエージェントさんのセミナールームで開催されたtry! Swift、その3日目に行ってきました。 海外からも100人を超える方々が参加され、とても活気のあるカンファレンスでした。

Swift言語にフォーカスした本カンファレンスですが、3日目は開発者テストに関するセッションが2つありました。圧倒的にスイフト力が足りない私なので、これらのセッションについてだけ書きます。

Swiftにおける実践的なモック化について

Veronicaさんのセッション。Objective-C動的言語なので、テストコードでモック(を含む、テストダブル)を使ってプロダクトコードやフレームワークの挙動をランタイムに置き換えることができていました。またそれを容易に実現するためのOCMockというライブラリもありました。

しかしSwiftではこれができないため(反面、安全と言えます)、どうモックを使っていくべきか?というセッションです。

そもそも、モックを使う理由

続いて、DI(Dependency Injection: 依存性の注入)を使う理由

  • カスタマイズ(置き換え)が容易にできる
  • オーナーシップが明確になる
  • テスタビリティ(試験性、テスト容易性)を上げる

テストダブルの代表的なもの

  • スタブ(メソッドの戻り値を置き換える)
  • モック(メソッドの呼び出し、引数を検証する)
  • パーシャル・モック(特定のメソッドだけ置き換え。これは『xUnit Test Patterns』の定義・分類ではなく、恐らくOCMock独自の言葉)

パーシャル・モックのアンチパターン

  • setUp()でモックを定義するのが大変になる
  • What's real? What's fake?

モックを自分で作る場合、本物のサブクラスを作るのでなく、そもそも必要なものをProtocolとして定義しておく。Javaでも原則Interfaceを書くという宗派がありましたが、それ*1

「ロールをモックせよ」の話。むやみにモック化しない。壊れやすいテストになる。

Swiftで将来的に使えるようになりそうな?モックフレームワークの紹介(リンクはぐぐったもの)

GitHub - rheinfabrik/Dobby: Swift helpers for mocking and stubbing

GitHub - DeliciousRaspberryPi/MockFive: A Mocking Framework for Swift Unit Tests

GitHub - mflint/SwiftMock: A mocking framework for Swift

GitHub - SwiftKit/Cuckoo: First boilerplate-free mocking framework for Swift!

CuckooはMockitoインスパイア系っぽい。

QA

  • 日付や時間に依存するテストはどう書くべき? -> 遭遇したことがないけど、考えてみるのも面白そう

@niwatakoさんの書き起こしも参照。

niwatako.hatenablog.jp

所感

Objective-Cでは(OCMock/OCMockitoでは)依存オブジェクトの扱いが雑な設計であっても、ランタイムにモック化することで強引にテスト可能だったりしたのですが、Swiftではちゃんと設計しないとダメだよね、という話。

またSwiftに限らず、モックは便利さにつられて乱用しがち、そしてフラジャイルな(壊れやすい)テストになりがち、という点まで、短い時間で正しく警告されていてすばらしいセッションでした。

モックに頼らない、という点は、設計の見直しで回避できるものももちろんありますが限界もあるので、より上層の(結合度の高い)テストレベルで担保するなどが最適解かな、と考えています*2。このセッションではそこまで詰め込めなかっただけだと思う。

なお、Swiftの一部メソッドを(Objective-Cの機能を使って)動的に置き換える手段について、Danielさんの"Code Injection from scratch"セッションで触れられていました。こちらも@niwatakoさんの書き起こし参照。

niwatako.hatenablog.jp

An Artsy Testing Tour

Ashさんのセッション。Artsyではこれまで4つのアプリに対してテストを書いてきたが、それぞれアプローチが違うので紹介する。

前提として、

ひとつ目のアプリ

  • テストは無かった。Apple TVローンチまで時間がなく、担当者一人で作ったもの。
  • テストを書くべきか否か、バランスを判断。コードベースが小さいものだったので、ローンチを優先した。

ふたつ目のアプリ

  • はじめテストは無かったが、コードベースが大きかったのでリグレッションテスト強化のため、後からテストを追加した。
  • "Bus factor"*3防止のため、テストのドキュメントとしての役割を重視*4
  • DIを多く使った
  • RSpec*5を使い、セットアップの共通しているテストはbeforeEachにまとめるなど、テストコードをリファクタリング。リーダブルなテストに。
  • 余りにネストしたcontext*6はおかしいので、設計を見直す。十分に説明的な名前がつけられるか?
  • テストはドキュメント
  • 振る舞いをテストする。コードをテストするのではない。

みっつ目のアプリ

  • これもはじめテスト無し、後で追加した。
  • ネットワークアクセスのあるアプリで、開発者も多く実装も散らばっていた。
  • はじめiPhone用、後にUniversal Appに。コンテキストが異なるので、一揃いのテストスイートを、iPhoneiPadふたつの観点からテストを実施した。
  • 大きなクラスからテストしたが、修正が入ると既存テストの書き換えも多発した。
  • Snapshot Test。画面のスナップショットを撮ってpngで保存し、ピクセルベースでdiffを取り合否判定*7。pull-requestの中にスクリーンショットがあるのでレビューしやすい。
  • 小さいクラスからテストすべき
  • 追加したコードにはテストを書く

よっつ目のアプリ

  • これは最初からテストを書いた。はじめてのSwiftアプリで手探り状態のところ、テストコードが拠り所になった。
  • テストツールにはQuick*8を使用。
  • Nimble*9というMatcherライブラリを使えば、assert()を、expect().to()形式で書けて可読性が高い。
    • Arrayなども直接比較できる。拡張もできるので、Snapshot用のMatcherを作った。

QA

  • TDDはハイコスト。クライアント側は手を抜いていいのでは?という意見があるが -> Swiftは型が強力なのでLLと比べてテストの重要性は低いが、でもコンパイラだけではチェックできない不具合を見つけられる。ドキュメントの価値もある。テスト大事。
  • Snapshot Testはどのテストレベルでやっている? -> 決めかねている。End to Endでなくてもいい。
  • TDDで「機能」を捉えるのが難しい -> TDDは賛否両論。Ashさんはどちらでもなくケース・バイ・ケース。繰り返しやっているとテスタブルなコードを書けるようになる。ロバストにはなりきらなくても、価値はある。小さく、小分けして、DI。繰り返しやってみること。練習である。

QAのメモはかなり怪しい。@niwatakoさんの書き起こしも参照。

niwatako.hatenablog.jp

所感

テストに対する姿勢、メリットが明確で、かつ4パターンの実際のアプリの事例を挙げられていることもあって、とても説得力のある、良いセッションでした。Spec BDD派が増えそう。

Spec BDDに限らず、可読性の高いテストはドキュメントとしても有効なので、ちゃんとメンテナンス、リファクタリングすべきですね。

また、質疑応答で出たTDDの話。プロダクトコードとテストコードを行き来することで、複数の視点でコードを見ることができるようになるはずなので、訓練だと思ってTDDをやってみるのは良いことだと思う。 後からテストを書くのは難易度が高いことが多いので、テストを書く習慣、テスタブルなコードを書く習慣をつけるのは良いこと。

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

先日のDroidKaigiもそうですが、開発者主体で、スポンサーも付けて、有料で、海外からも参加者およびスピーカーが来てくれる。ほんの2〜3年前には考えられなかったことで、それを実現された主催者・関係者の尽力はすごいと思います。

改めて、お疲れ様でした。ありがとうございました。

こうして(ごく一部のセッションの)ブログを書くことくらいしかできませんが、少しでも盛り上がりに貢献して、次回以降につながればいいな、と。

最後に蛇足ながら。Twitterで「本カンファレンスが1スレッド進行なのが良かった」という話がありました。完全に同意!なのですが、それは今の時期のSwiftだから、という条件付きかも。マルチセッションがすべて悪、みたいな方向には行かないで欲しいかな。

*1:Javaでは「C言語のヘッダファイルみたいに増える」という批判がありましたが、少し前までObjective-Cで書いていたから抵抗ないはず?

*2:モック愛好家のポジショントーク注意

*3:日本だとトラックを使いますよね?

*4:いわゆる仕様化テスト

*5:と言っていたけど恐らくKiwiかな?

*6:describeと同義

*7:FacebookOSSとのことなので、恐らく https://github.com/facebook/ios-snapshot-test-case を使用

*8:RSpec, Kiwiと同様、Spec BDDライブラリ。 https://github.com/Quick/Quick

*9:https://github.com/Quick/Nimble

Google Cloud Messaging (GCM) の新機能 Topic Messagingを試したメモ

Google I/O 2015で発表された Google Cloud Messaging(以下GCM)の新機能、Topic Messagingを試してみました。

環境、前提条件などは、先の記事を参照してください。

nowsprinting.hatenablog.com

Topic Messaging とは

Topic Messagingとは、従来のGCMおよびApple Push Notification Service(以下APNs)のような送信先デバイスのトークンを指定してのPush通知ではなく、Publish/subscribeモデルのPush送信を行なう機能です。

Publish/subscribeモデルとは、各デバイスから購読(subscribe)したいトピックをあらかじめ登録することで、メッセージ送信(publish)側では個々のデバイスを意識することなく、トピックに対してメッセージを送ることで購読デバイスすべてにメッセージを配信できるものです。

これによって、例えば、

  • ニュースの更新などをPush通知したいだけであれば、サーバアプリケーションでは受信デバイスのRegistration Tokenを管理する必要がない
  • 送信機能もクライアントアプリに持たせる場合、サーバアプリケーション自体を立てる必要がない

といった構成が実現できるようになります。

制約・制限

GCMのTopic Messagingには、以下の制約・制限があります。

  • トピックのキーは、/topics/[a-zA-Z0-9-_.~%]+の書式
  • アプリあたり*1の購読数は100万件まで
  • トピックに送信できるメッセージのペイロードは2KBまで

購読数については、ユーザが1トピックづつ購読して100万ユーザまで、平均2トピックなら50万ユーザまで…となるので、インストール数が見込まれるアプリでは注意が必要です。

トピックの購読

以下、前回同様 Try Google Cloud Messaging for iOS のサンプルを使用して試していきます。

トピックの購読は、次のメソッドで行ないます。(メインスレッドから呼ぶ必要があります)

[GCMPubSub subscribeWithToken:topic:options:handler:]

サンプルのGcmExampleでは、起動時にトピック/topics/globalを購読するようになっています。

トピックへのメッセージの送信

続いて、同じく"Get started"のサンプルプロジェクトにあるGcmServerDemoを起動すると次のウィンドウが表示されます。

f:id:nowsprinting:20150607043716p:plain

apiKey欄にGCMのAPI Keyを入力し、"Send to topic"ボタンをクリックすると、送信メッセージの(本来、宛先のRegistrationTokenを設定する)toに、/topics/globalを設定して送信が行われます。

具体的には、以下のメッセージが送られます。

[
    "to": "/topics/global",
    "notification": [
        "body": "Hello from GCM"
    ]
]

これだけで、トピック/topics/globalを購読しているデバイスすべてにPush通知が送信されます。

トピックメッセージの受信

受信は通常のPush通知と同様です。受信側でトピックメッセージ固有の処理を行いたい場合は、 [UIApplicationDelegate application:didReceiveRemoteNotification:]メソッドで以下のように判定できます。

-(void)application:(UIApplication)application
        didReceiveRemoteNotification:(NSDictionary)notification {
    if ([notification[@"from"] hasPrefix:@"/topics/"]) {
        // トピックメッセージを受信した
    } else {
        // 通常のメッセージを受信した
    }
}

なお、今のところ、購読されているトピックのリストを取得するようなインタフェースは、サーバ側にもクライアント側にも定義されていないようです。

その他、エラーハンドリングなど詳細は公式のガイドを参照してください。

Implementing Topic Messaging

App nameとBundle ID, Androidアプリとの関連

GCMの利用登録を行なうページでは、"App name"と"iOS Bundle ID"を入力します。ここで、"App name"は、Google Developer Console(API Console)の"Project name"となります。

f:id:nowsprinting:20150615112845p:plain

これに対し、"iOS Bundle ID"は1:nの関係で登録することが可能です。 同様にAndroidアプリ場合、Bundle IDの代わりに"Android package name"欄にパッケージ名(ApplicationID)を登録しますが、これも1:nの関係になります。

トピックメッセージを送信するためのAPI Key、およびSender IDは、"App name"単位に割り当てられます。これに、複数の"iOS Bundle ID", "Android package name"を紐付けられますので、iOS版とAndroid版、また、Lite版やOEMしたアプリなどに対し、一括してトピックメッセージを送信することができます。

なお、iOSアプリ向け設定ファイルであるGoogleService-Info.plistにはSender IDしか書かれていませんので、複数のビルドターゲットで共有することができます。

Androidアプリ向けのgoogle-services.jsonには、Sender IDのほかに登録されたパッケージ名が書かれますが、登録されたパッケージ名が全て配列で書かれるようです。 そのため、フレーバーを定義して複数のアプリをビルドする場合でも、ひとつのgoogle-services.jsonを共有して問題ありませんでした。

*1:おそらく、Google APIs ConsoleのProject単位のこと

Google Cloud Messaging (GCM) でiOSデバイスにPush通知を送る

Google I/O 2015で発表された、Google Cloud Messaging(以下GCM)を使ってiOSデバイスにPush通知を送る手順が公開されていたので試してみました。

developers.google.com

なお、GCMを使うと言っても最終的にiOSデバイスにPush通知を行うのは従来通りiOS標準のApple Push Notification Service(以下APNs)です。 従来、サーバアプリケーションが直接デバイストークンを管理し、APNsにメッセージを直接送信していましたが、サーバアプリケーション→GCM→APNsと、GCMを介してPush通知を行なうことができます。

GCMにおいて端末固有のトークンとなるRegistration Tokenは、iOSの(APNsの)Device Tokenとは異なる、次のような*1文字列です。

bk3RNwTe3H0:CI2k_HHwgIpoDKCIZvvDMExUdFQ3P1pLTQ/8t-5QNiXbYwZYEWiSFD-frQKlsV8lgI

iOSアプリでGCMを利用するメリットとしては以下が挙げられます。すでにMBaaSのPush通知サービスなどを使っているのであれば、実現できているものだと思います。

  • iOSAndroidのデバイストークン(Registration Token)を一元管理できる
  • iOSAndroidのPush送信ロジックを共通化できる
  • Topic Messagingを利用することで、デバイストークン指定でなくトピック文字列指定で該当するデバイスにPush通知できる

環境

今回試した環境は以下の通りです。

  • OS X 10.10.3
  • Xcode 6.3.2
  • iOS 8.3(Push通知を受けるには実機である必要があります。シミュレータはNG)
  • GCMクライアントライブラリ 1.0.7

なお、GCMクライアントライブラリを利用するには、iOS 7以上が必要です。*2

事前準備

前述の通り、APNsを利用することになりますので、App IDごとの"Push Notification"を許可し、APNsのSSL証明書を生成しておく必要があります。

証明書は、Development(sand box)、Productionいずれも利用可能です。今回はDevelopmentのみ試しています。

サンプルアプリのビルド

以下、Try Google Cloud Messaging for iOSの手順について補足します。

  1. CocoaPodsでサンプルを取得します

    $ pod try Google

    使用するサンプルを聞かれるので、 4: Samples/gcm/GcmExample.xcodeprojを選択。Xcodeでサンプルプロジェクトが開きます。

    サンプルプロジェクトには、クライアント二種類(Objective-C版とSwift版)、送信側のMacアプリケーションの、計3つのビルドターゲットが含まれます。

  2. Google Developersサイトで、GCMのAppと、iOSアプリのBundle IDを登録します

    Try Google Cloud Messaging for iOSページにある"GET A CONFIGURATION FILE"ボタンで登録ページに遷移できます。

    ここでApp Name(Androidアプリと共有するならば同じ名前で)、アプリのBundle IDを入力し、続いてAPNsのSSL証明書(p12形式)をアップロードします。すると、API KeyとSender IDを取得でき、GoogleServices-Info.plistがダウンロードできます。

  3. GoogleServices-Info.plistをサンプルアプリ(クライアント側)に取り込みます

    GoogleServices-Info.plistには、Sender IDが書かれています。

  4. サンプルアプリ(クライアント)を実行します

    実行すると、GCMにRegistration Tokenが設定され、Push通知を受け取る準備が整います。また、Topic Messagingのためのトピック/topics/globalにも登録されます。

    なお、このときコンソールにRegistration Tokenが出力されていますので、文字列をコピーしておきます。

送信サンプルの実行

サンプルプロジェクトのGCMServerDemoが送信側のサンプルです。起動すると下図のウィンドウが表示されます。

f:id:nowsprinting:20150607043716p:plain

apiKey欄にGCMのAPI Key、Registration TokenにクライアントのRegistration Tokenを入力し、"Send notification"ボタンをクリックすると、該当するiOSデバイスにPush通知が届きます。

f:id:nowsprinting:20150607044650p:plain

送信フォーマット

GCMに渡すメッセージが、APNsの形式に置き換えられてデバイスに届きます。例えばサンプルでは以下のフォーマットをGCMに送っています。

[
    "to": to,
    "notification": [
        "body": "Hello from GCM"
    ]
]

デバイス側ではPush通知のuserInfoとして、以下の形式で受け取ることができます。

aps =     {
    alert = "Hello from GCM";
};
"gcm.message_id" = "0:1433613206212203%4deaf3f24deaf3f2";

badge, sound

badgeとsoundも以下のように指定できました。

[
    "to": to,
    "notification": [
        "body": "Hello from GCM",
        "badge": 10,
        "sound": "default"
    ]
]

iOSデバイスのアプリアイコンにバッジが付き、またPush通知の受信音が鳴ります。 またアプリでは下記形式で取得できました。

aps =     {
    alert = "Hello from GCM";
    badge = 10;
    sound = default;
};
"gcm.message_id" = "0:1433613206212203%4deaf3f24deaf3f2";

その他のパラメタ

上記以外のパラメタは、notification下、もしくはdata下に設定することでアプリに渡すことができます。

[
    "to": to,
    "notification": [
        "body": "Hello from GCM",
        "key2": "value2"
    ],
    "data": [
        "key1": "value1"
    ]
]

アプリでは下記形式で取得できました。

aps =     {
    alert = "Hello from GCM";
};
"gcm.message_id" = "0:1433613206212203%4deaf3f24deaf3f2";
"gcm.notification.key2" = value2;
key1 = value1;

notification下に書いたkey2は、受信側ではgcm.notification.key2というキーに置き換わっています。構造を見る限り、data下に置くほうがよさそうです。

ペイロードに指定できる内容の詳細はリファレンスを参照してください。

Notification payload support

Topic Messaging

Topic Messagingについては次の記事を参照してください。

nowsprinting.hatenablog.com

*1:GCMのサイト内にサンプルとして掲載されていたもので、実在のトークンではありません

*2:ただし、サンプルアプリをそのままiOS 7で動作させるとクラッシュします。iOS 8で導入された[UIApplication registerUserNotificationSettings]を呼んでいるためで、iOS 7以前では[registerForRemoteNotificationTypes]を使用するように書き換える必要があります