やらなイカ?

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

Tokyo HoloLens ミートアップ vol.4に参加してきました #HoloLensJP #TMCN

Tokyo HoloLens ミートアップ vol.4に行ってきました。参加するのは通算3回目(のはず)。

hololens.connpass.com

以下、各セッションのメモ。残念ながらLT前に離脱したのでメインセッションのみ。

俺たちが作るべきMR

ゆーじ@yuujiiさんの、例のハッカソンで例のVRアーティスト賞を受賞したHololensを使ったお絵描きアプリにまつわる話。

www.slideshare.net

  • すぐ近くの3Dオブジェクトを描画しない設定であるnier clipについて。HoloLensでは0.85(より近いものは描画しない)だが、ホワイトボードに投影しながら絵を描くという性質上0.3にした
  • 0.3より低くすると、ホワイトボードに近づきすぎてしまいトラッキングを失うため、あえて0.3とした

ARもVRもMRもまとめてドドンドーン!

前本 知志@peugeot106s16さんの、KINECT等を使って位置同期を行なう話。

www.slideshare.net

  • ARアプリでは、3DモデルとKinectが捉えた物体とのzテストでオクルージョンできる
    • 現実ではオクルージョンされるのが当たり前なので、誰もすごいと言ってくれない!(気づいてもくれない!)
  • 複数KINECTでの位置同期
    • GPSと同じ三点測位で、リアル世界にある原点に対する、KINECTカメラの相対位置を求める
    • depthが取れればKINECTでなくてもできる
  • KINECT, HoloLens Tangoで位置を同期する
  • 現実世界を基準に、仮想世界もそこに重ねる。各デバイスは覗き窓となる

多人数同期MR体験を実現するプラットフォームDAHLES

MIRO@MobileHackerzさんの、N高入学式で使用したHoloLens数十台運用プラットフォームの話。

  • 1対nで配信
  • 大域自己位置ポジショニング
    • HoloLensのアンカーの問題点
      • 全端末がspatial mapを持ってないとできない
      • 大規模会場ではdepthが届かない(ぜいぜい数メートル)
      • 似てると間違える。列が一列ずれる。四角い部屋だと90度・180度ずれることも
      • 一度誤認識されると、なかなか再認識されない
    • 可視光マーカーを併用する
      • マーカーで大まかな位置を知り、近くのアンカーを使って精細なポジションを得る
  • 無線ネットワークトラフィック管理・配信コンテンツ管理
    • 輻輳防止
    • 隠れ端末問題
    • ブロードキャストパケットによるイベント同期
    • どうしてもムリなところは無線を強くする
  • HoloLens端末の運用管理
    • アプリケーションのデプロイ
      • Device Portalを使うと結構できるけど、無線は遅いのでつらい
      • USBでもできるようにした
    • 装着オペレーションのサポート
      • 「こう見えていれば正解」という画面を装着案内時に表示しておくのは有効

VR ZONE SHINJUKUに行ってきました

以前お台場で試験営業していたVRアクティビティ体験施設 "VR ZONE" が、新宿に新たに "VR ZONE SHINJUKU" としてリニューアルオープンしたので、早速行ってきました。

体験したアクティビティは、マリオカート、ハネチャリ、釣り、ガンダムの順。やはり全体にお台場時代からあるものの人気は低めなようで、逆に人気の高いのは(土曜18時に入場した時点で)釣りとエヴァンゲリオンが60分待ち、帰る頃にはエヴァンゲリオンのみ並んでいるような状況。

ただ、いずれのアクティビティも筐体の台数は多く(例えばマリオカートは4台×3セット)、HMDなどの着脱を行なうスタッフさんが1対1で付くかどうかで回転率は大きく変わるため、かなり柔軟にスタッフさんの移動を行って*1待ち行列をコントロールしようとされているようでした。

アクティビティ

個々のアクティビティの内容や感想は割愛しますが、総じて酔いにくく、かつ楽しめるものになっていました。特に、2台や4台セットになっているアクティビティでは、ヘッドセットにマイクが付いておりプレイヤー間で会話できて盛り上がりの一助となっていました。

それもあって集団で行くのが楽しいとは思うのですが、逆に少人数の場合2〜3人グループの空いた席に入れてもらえる率が高く、あまり待たずに回れるというメリットもありそうです。

機材面では、マリカーも釣りも、(Viveコントローラーではなく)Vive Trackerを使用。特に、釣りの竿コントローラーは前評判通りすばらしい体験でした(ハード面は下記記事を参照)。

www.nikkan.co.jp

スタッフ

上にも書きましたがスタッフさんの練度は総じて高く、ストレス無く体験できました。また写真の撮影を引き受けてくれたりもしました。感謝。

チケット

チケットはアプリ版を使ったのですが、良かったところは入場ゲートをそのまま通れるところ。逆に下記デメリットがあり利便性は今ひとつ。

  • 数人分まとめて購入した場合でも、結局全員が個々にアプリをインストールし、チケット分配機能で分配する必要がある
  • アクティビティ利用時のチケット表示のたびに通信が必要
  • アクティビティごとに必要なチケットの種類が4種(赤・青・黄・緑)あるが、該当するものをフリックで選択してスタッフさんに提示する必要がある(必要なチケットの判定ができているのだから、自動的に消し込みして欲しかった)
  • アクティビティチケット単位での譲渡はできない(端末ごと貸せばできますが、紙ほど手軽ではない)

セガさんのところのように、チケットにゲームのスコアが紐付いて後からチェックできるような付加価値でもあれば相殺していい程度の不便さなのですが、改善されると嬉しいです。

まとめ

ローケーションも(新宿フェイスのすぐ近くで)良いですし、アクティビティの質・量ともにお台場時代より強化されています。お台場のとき行ったことがある人も、ない人も、行って体験してみるといいと思います!

vrzone-pic.com

*1:スタッフさんは多くのアクティビティを知っていなければならず大変だと思います

LINE Messaging APIでグループ内の発言ユーザIDが取得できるようになった件 #linebot

これまで、LINE Messaging APIで組んだBOTをグループやトークルームに参加させた場合、すべてのユーザの発言はそのグループもしくはルームが発言元となり、実際に誰の発言であるかを知ることはできませんでした。

5/31にリリースされた新機能でこの制約が解消され、グループ/ルームにおいても(groupId/roomIdとは別に)発言者のユーザID(userId)を取得することができるようになりました。

ただし、以下の制約があります。

  • 発言ユーザのクライアントのバージョンが、iOS/Androidとも、7.5.0以降であること
  • 発言ユーザが“Official Accounts Terms of Use”に同意していること

これらを満たしていれば、コールバックで受け取ったjsonsource下にuserIdが格納されます(Golang SDKの場合、eventのSource.UserIDで取得できます)。

満たしていなければ、従来通りuserIdは入ってきません。

Official Accounts Terms of Useへの同意

Official Accounts Terms of Useへの同意は、以下のような契機で表示される認証画面から行ないます。

  • 新たにBOTを友だち登録する
  • すでに友だち登録しているBOTトーク画面(グループやルームではなく、1:1のトーク)を開く

これは、必ずしもBOT個々に行なう必要はなく、例えば、謎の女子高生AI「りんな」に対して同意済みのユーザであれば、自作のBOTでもユーザIDを取得することができます。

f:id:nowsprinting:20170613045546j:plain

この画面下にある「同意する」をタップすると、同意が完了します。

正直なところ、一般の人向けのグループにBOTを混ぜて運用している場合、この手順を全員にアナウンスし実行させたり、ユーザIDが取得できないケースをBOT側で考慮する必要があるというのは少々ハードル高いですが、時間が解決してくれることに期待しましょう。

プロファイル取得の制限 [6/24追記]

上記の手続きでユーザIDは取得できますが、ユーザプロファイル(表示名、画像、ステータスメッセージ)の取得にはさらに制限があります。

ユーザプロファイルはhttps://api.line.me/v2/bot/profile/{userId}で取得しますが、これにBOTと一度も友だち登録されていないユーザのIDを指定した場合、404エラー(Golang SDKの場合linebot: APIError 404 Not found)が返ります。

"一度も友だち登録されていない"と書きましたが、一度でも当該BOTと友だち登録を行なえば、その後ブロックされても取得できるようです*1*2

[10/1追記] グループ/ルームメンバーのプロファイル取得のためのAPIが7月に追加されています。こちらのエントリを参考にしてください。

nowsprinting.hatenablog.com

多人数のグループなど、BOTとの友だち登録を徹底するのが難しい場合、あらかじめ(もしくは404を受けて)LINE Loginに誘導してログインさせてプロファイルを取得しDBに保持、BOT側ではWebhookで受け取ったユーザIDで突合*3して使うほうが運用負担は低いように思います。

※このセクションは、『LINE BOTを作ろう! Messaging APIを使ったチャットボットの基礎と利用例』の著者、立花さんとのやり取りで発覚し、調査・追記しました。

LINE BOTを作ろう!  Messaging APIを使ったチャットボットの基礎と利用例

LINE BOTを作ろう! Messaging APIを使ったチャットボットの基礎と利用例

参考

*1:ただしこの振る舞いはドキュメントに明記されているものではなく、今後アナウンスなく変更される恐れはあります。そもそも「友だち登録していなければ404」という振る舞いもどこにも書かれていないのですが。

*2:10/1時点ではこの振る舞いは変更されており、友だち登録した後ブロックした場合にも404が返されるようになりました

*3:LINE LoginとMessaging APIで受け取れるUserIdは同じユーザで同一のものが得られます

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

かえりマン

台湾のVR体験施設『VIVELAND』を見てきたメモ

武術関係の用事で台湾に行ったついでに、期間限定オープンしているHTCのVR体験施設『VIVELAND』を見てきました。

VIVELANDは、台北市における秋葉原 ポジションである『光華商場』ビルの隣、『三創生活園區』の3Fにありました。このビル、他にはHTCやMSIのショップが入っています。オーナーは鴻海だそうです。

三創生活園區の場所はここ。地下鉄駅からすぐ。

3Fの一角にVIVELAND

f:id:nowsprinting:20161113055443j:plain

通路側から見えるところに、制限時間内にいくつかのVIVEコンテンツを遊べるブース。200NTD(およそ600JPY)で15分間。物価*1を考えると強気な設定。

f:id:nowsprinting:20161113055409j:plain

写真では見づらいですが、アーチ状の天井に可動式の竿みたいなものが生えていて、そこからHMDのケーブルが出ています。 ベースステーションもアーチのところに設置。

コンテンツは、Steamで買えるもののほか、オリジナルのものもあるみたい。

※表の下3つは専用ブースのものなので、自由に遊べるものの対象外。

一番人気らしい*2、『Project CARS』。250NTD(およそ750JPY)/1ゲーム。これも通路側に設置。

二番人気、FPSの『FRONT DEFENSE』。200NTD(およそ600JPY)/1ゲーム。これも通路側。左手の壁から竿が延びていて、HMDのケーブルがつながっています。上にあるモニタは、プレイヤーのHMDに映っている映像が流れていました。

土嚢に隠れたり、地面から何か拾ったり、忙しそうなFPSでした*3

受け付けを通って奥に入ると、こんな感じのブースが4つ。オープンして間もない平日昼間ということで、まだここまで入ってくるお客さんは少ないみたい。

一番奥には、どこかで見たことがあるような、細い板を渡るっぽいブースが!

この高所恐怖SHOW『命懸一線』というコンテンツはすぐ体験できたので、やってみることに。150NTD(およそ450JPY)/1ゲーム。

受け付けでお金を払うと、これまたどこかで見たような、でもちょっと違うマスクを渡されます。へーこれを被ってからHMDを着けるのかー(棒

ブースに戻ると、板の手前に立たされ、足にマジックテープでVIVEコントローラーを取り付けてもらいます。手には何もなし。

HMDとスピーカーを装着してプレイ開始。なんとなく高所恐怖SH◯Wと比べてしまいますが、

  • スタートは、まず足元を見て、続いて正面の円を見ることで周囲(ビルの屋上)が描画されて開始。エレベーターで上昇するような演出はなし。
  • 猫を助ける(行って戻る)のではなく、背後から追い立てられるという趣向
  • 導線は弱く、お姉さんに「後ろを見て」と言われてはじめて趣旨を理解する始末。台湾語ではちゃんと導入があるのかも知れない(一部スマートフォンの翻訳を使いつつの英語で説明してくれてたので)
  • 板は不安定になっていて、ちゃんとガタガタ揺れる
  • 板の先には当初なにもないけれど、ヘリコプターが迎えに来るので乗り移る(半歩で届く至近距離に無風でホバリングするヘリにプレゼンスは無い)。

所感

お台場の『VR ZONE』でもHTC VIVEを使用していた関係で、結構オフィシャルにノウハウを吸い上げていたのではと予想していたのですが、少なくともコンテンツの作りはまだ色々足りていない感じ。

スタッフの練度もまだ低いかも知れませんが、みんなフレンドリーに接してくれて印象はよかった。

平日の昼間ということもあり、お客さんは通路沿いの『Project CARS』と『FRONT DEFENSE』だけ数名の列、ほかは2組だけとまばらでした。現地の新聞やテレビのニュースでも取り上げられていたそうで、これから先、もっとお客さん増えて台湾のVRも盛り上がるといいですね。

参考

www.vive.com

www.moguravr.com

HTC、台北の三創生活園区にHTC VIVEを体験できる「VIVELAND」を期間限定でオープン – PANORA

htc.hatenablog.com

*1:地下鉄初乗りが20NTD、缶ビールが40NTDくらい

*2:スタッフのお兄さん談

*3:これもプレイしたかったのですが、無理を言っての別行動だったので並んでまでプレイする時間は無かった

GAE/Goで動くLINE BOTのテストを書いてみた

Google App Engine Go(以下GAE/Go)上で動くLINE BOT調整さんリマインダBOT」のMessaging API対応やグループ対応をしつつ、テストを書いて得た知見のメモ。

環境

環境変数

API KEYなどをapp.yaml環境変数として定義している場合、テスト実行($ goapp test)では値を取得できません。

なので、Makefileにテスト用の値を定義し、常に$ make testで実行するようにしました。

export LINE_CHANNEL_SECRET=012345678901234567890123456789ab
export LINE_CHANNEL_ACCESS_TOKEN=u012345678901234567890123456789ab

test:
    goapp test -v

実際のMakefileでは、カバレジ取得、-runオプションの付与も行なっていますが割愛。

aetest package

プロダクトコード内でappengine.NewContext(http.Request)context.Contextを取得している場合、httptest.NewRequest()などで生成したhttp.Requestを渡すとエラー*1になります。

そのため、GAE/Goのテストではaetest packageを使用してcontext.Contexthttp.Requestを生成して使う必要があります。

Contextだけが必要な場合

データストアに関するテストを書く場合などContextだけが必要な場合は、以下のように取得できます。

c, done, err := aetest.NewContext()
if err != nil {
    t.Fatal(err)
}
defer done()

aetestを使用すると、GAE/Goのローカル開発サーバ( local development server)が起動します。

これには都度(テストケースごとに)起動に時間がかかるのと*2defer done()を忘れるとプロセスが起動したまま残ってしまう*3ので注意しましょう。

Instanceが必要な場合

http.Requestを使うテストの場合には、以下のようにGAEインスタンスを直接取得します。引数のaetest.Optionsは、データストアを使用しないならnilでも構いません(後述)。

opt := aetest.Options{StronglyConsistentDatastore: true}
instance, err := aetest.NewInstance(&opt)
if err != nil {
    t.Fatalf("Failed to create aetest instance: %v", err)
}
defer instance.Close()

こちらも、defer instance.Close()を忘れずに。

取得したインスタンスから、以下のようにhttp.Requestcontext.Contextを生成できます。

req = httptest.NewRequest("POST", "/line/callback", json)
c := appengine.NewContext(req)

なお、appengine.NewContext()は、例えばプロダクトコード内とテストコード側で二回記述されていても、インスタンスが同じなので同一のデータストアを扱うことが出来ます。プロダクトコードの引数にhttp.Requestがあれば、無理にcontext.Contextまで渡す必要はありません。*4

http.RequestのContent-Typeヘッダ

テストに使うhttp.Requestを自力で組み立てる場合、以下のようにContent-Typeヘッダを付与しないとBodyが渡りません。

Task Queueなど、url.ValuesをEncode()する場合

req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

jsonの場合

req.Header.Set("Content-Type", "application/json")

WebhookのX-LINE-Signatureヘッダ

LINE Messaging API SDKにパースさせるWebhookのリクエストには、リクエストヘッダにSignatureを乗せる必要があります*5。 Signatureは、API Referenceの"Webhook Authentication"に書かれている検証手順を元に、以下の手順で生成できます。

  1. Channel Secretを秘密鍵として、HMAC-SHA256アルゴリズムによりRequest Bodyのダイジェスト値を得る
  2. ダイジェスト値をBASE64エンコードした文字列を、Request Headerに付与する

具体的には以下のコードで生成・付与できます。

channelSecret := os.Getenv("LINE_CHANNEL_SECRET")
hash := hmac.New(sha256.New, []byte(channelSecret))
hash.Write(byteBody)
encoded := base64.StdEncoding.EncodeToString(hash.Sum(nil))
req.Header.Add("X-LINE-Signature", encoded)

httpmock

httpmockは、テスト実行時に外部サーバのスタブとして動作します。Goではhttptest.NewServer()で簡単にスタブを立てることはできるのですが、今回のようにアクセス先URLがSDK内に隠蔽されているケースなど、アクセス先URLを書き換えずにモックできるので便利です。

本家(jarcoal/httpmock)はメンテナンスが止まっているので、forkされて継続開発されているこちらを使用しました。

github.com

httpmockの有効化

以下のコードで有効化できます。単にhttpmockを有効化すると、以降すべてのURLに対するリクエストをhttpmockが受け取り、エラーレスポンスを返すようになります。

ctx := appengine.NewContext(req)
client := urlfetch.Client(ctx)

httpmock.ActivateNonDefault(client)
defer httpmock.DeactivateAndReset()

ここで重要なのは、引数付きのActivateNonDefault()を使い、appengine.urlfetchインスタンスを渡している点です。

GAE/Goでは、外部へのhttpリクエストは(DefaultClientではなく)urlfetch.Client()で生成したクライアントから送る必要があります。そのため、linebotクライアントもurlfetchを使って初期化しています(過去記事「調整さんリマインダLINE BOTを作ってみた - やらなイカ?」参照)。

httpmockはhttp.Clientインスタンスに対して効力があるため、linebotクライアントに渡したものと同じインスタンスActivateNonDefault()に渡す必要があります。

スタブの定義

例えば、LINEのReply Message APIが単に正常終了するスタブであれば、以下のように記述します。

httpmock.RegisterStubRequest(
    httpmock.NewStubRequest(
        "POST",
        "https://api.line.me/v2/bot/message/reply",
        httpmock.NewStringResponder(200, "{}"),
    ),
)

以降、指定したURLに対するリクエストにはステータスコード200、ボディに"{}"が返るようになります。なお、URLは完全一致で、URLパラメタ(?key=value)も含めて同一の文字列である必要があります。

スタブは、必要なだけいくつでもRegisterStubRequest()で追加できます。

[10/31追記]同一のURLを持つスタブを複数RegisterStubRequest()してもエラーにはなりませんが、そのURLに複数回リクエストを発行しても、常に最初のスタブが使われます。従って、後述のAllStubsCalled()でエラーとして検知されます。

スタブが呼ばれたことを検証する

想定したURLすべてに対して正しくリクエストが送られたことを確認するには、AllStubsCalled()を使います。

if err := httpmock.AllStubsCalled(); err != nil {
    t.Errorf("Not all stubs were called: %s", err)
}

定義したのに呼ばれていないスタブがひとつでも存在するとErrorを返してくれます。呼ばれていないスタブが複数あるときにはErrorに全て列挙してくれます。

なお、リクエストの内容(たとえばLINEに送信したText Messageの内容)まで検証したければ、スタブのNewStubRequest()の第3引数に直接無名関数を書くことで検証が可能です(httpmock - GoDocに例があります)。

その他のTips

Goのテストではあたりまえのことかも知れませんが、その他、知ったこと。

Error()とFail()

  • t.Error()では、テストコードの実行は継続されるが、テストはFAILする。構造体メンバの検証、パラメタライズドテストなど、一度の実行で問題点を全て知りたいときに有用
  • t.Fail()は、テストがFAILし、テストコードの実行が中断される。テストフィクスチャ構築中や、その他、継続しても仕方のない箇所でのエラーに使う
  • t.Log()で出力したログは、テストがFAILしないと出力されない

-run オプション

  • $ go test -run 関数名とすると、関数名に一致するテストだけ実行できる

TODO

今後なんとかしたいこと。

Task Queueのテスト

Task Queueに関するテストが書けない(taskqueue.Add()されたことを検証できない)件。

  • taskqueueをモックすればできるのかも?
  • そもそも、このBOTで個々の処理をTQにする必要はなかったのでは。goroutineを使うべき?

バージョン番号の埋め込み

『みんなのGo言語』p.56に書かれていた、ビルド時の-ldflagsgit describe --tagsで取得したバージョン番号を埋め込む方法を試しましたが、GAE/Goでは指定できず。goapp deploy時に指定しても無駄でした。

とりあえず、Makefile中でversion.goファイルを書き出すことで実現。

依存パッケージ管理

Glideで依存パッケージ管理をしようと試みましたが、deploy時にエラーが出てしまい断念。下記エントリに従えばできそうなので、いつかリトライ予定。

静的解析

gometalinterをローカルおよびTravis CI上でもafter_successで実行していますが不完全。

  • warningだけでも終了コード>0が返るため、scriptで実行させられないのが現状。warningを全部取るか、細かくパラメタ設定するか
  • Travis CI上では、ローカルでは出ないerrorが出る。正しくvendoringする必要がありそう

参考

関連エントリ

書籍

みんなのGo言語[現場で使える実践テクニック]

みんなのGo言語[現場で使える実践テクニック]

第6章 Goのテストに関するツールセット

システムテスト自動化 標準ガイド (CodeZine BOOKS)

システムテスト自動化 標準ガイド (CodeZine BOOKS)

  • 作者: Mark Fewster,Dorothy Graham,テスト自動化研究会,伊藤望,玉川紘子,長谷川孝二,きょん,鈴木一裕,太田健一郎,森龍二,近江久美子,永田敦,吉村好廣,板垣真太郎,浦山さつき,井芹洋輝,松木晋祐,長田学,早川隆治
  • 出版社/メーカー: 翔泳社
  • 発売日: 2014/12/16
  • メディア: 単行本(ソフトカバー)
  • この商品を含むブログ (3件) を見る
第14章 CI(継続的インテグレーション

*1:panic: appengine: NewContext passed an unknown http.Request

*2:ログ出力のためだけにContextを渡しているのであれば考え直したほうがよさそう。テスト実行時ならfmt.Print()でも視認できるので

*3:ローカル開発サーバの生き残りプロセスは $ ps -ef | grep dev_appserver.py で確認できます

*4:調整さんリマインダBOTではこのあたりキレイに書けておらず、いずれリファクタリングします。不慣れな言語こそTDDしないとダメですね

*5:クライアント側でSignatureを検証する必要があり、その機能までSDKに実装されているため