やらなイカ?

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

実践 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に実装されているため

LINEの新しいMessaging APIを試してみた

先日開催されたLINE DEVELOPER DAY 2016で発表された*1BOT APIに替わるMessaging APIを試してみました。

business.line.me

まず、先日作成した「調整さんリマインダBOT」をMessaging APIに移行。ついでに、新機能であるTemplate Messageと、グループやトークルームにBOTを参加させたときの振る舞いを確認します。

nowsprinting.hatenablog.com

Messaging APIへの移行

BOT用アカウントの作成

BOT API Trialのアカウントは利用できないので、新規にビジネスアカウントを開設します。手順は以下の通り。

  1. LINE Business Center -> アカウントリスト -> ビジネスアカウントを作成する
  2. 作成したアカウントの LINE@ MANAGER -> アカウント設定 -> Bot設定
  3. APIを利用する”を選択(この時点でアカウントは、LINE@アプリ、1:1トーク、お店トークが利用できなくなります)
  4. 必要に応じてBotの設定を行なう
    • Webhook送信(デフォルトは"利用しない”)
    • グループトーク参加(デフォルトは"利用しない”)
    • 自動応答メッセージ(管理画面で設定。デフォルトは”利用する”)
    • 友だち追加時あいさつ(同上)

自動応答メッセージおよび友だち追加時あいさつは"利用する"であっても、ちゃんとWebhookが飛びます。メッセージの内容は「メッセージ」から変更できます。

その他、LINE@ MANAGERでBOTのアカウントページなども設定できます。

App Engineアプリケーションの作成

App Engineアプリケーションは(version指定するなどして)使いまわしても良かったのですが、今回は新規に作成しました。

先の記事と同様にWebhook URLを設定し、Channel SecretChannel Access Token*2の取得を行ないます。Channel IDおよびChannel MIDは不要になりました。

BOT SDKの更新

Golang用のSDKもアップデートされているので、下記コマンドで更新しておきます。

$ goapp get -u github.com/line/line-bot-sdk-go/linebot

BOT本体の変更

主な変更点は以下の通り。

  • LINE BOTの初期化において、Channel IDおよびMIDが不要になったので削除
  • 代わりにACCESS_TOKENが必要になったので追加
  • linebot.OpTypeがなくなったので、stringに変更
  • OpTypeの判定は全てlinebot.EventTypeに変更。ここの判定はかなりスッキリした
  • APIの変更に対応
    • 大きな変更点として、ユーザの発言に応答するReply Messageと、BOTが起点となって発信するPush Messageに二分されました。今回のBOTは仕様上Pushを利用しましたが、通常のBOTではReplyを使っていくことになるでしょう。
  • テストの修正
    • X-Line-Signatureヘッダキーの変更(もとはX-LINE-ChannelSignature)
    • 友だち追加テストデータ(json)のフォーマット変更

以上でテストを通し、goapp deployして動作確認を行ないました。詳細・差分はGitHubに置いてあるソースを見てください。

github.com

なお、旧BOT APIで取得できていたユーザの識別子であるMIDは、Messaging APIで取得できるUserIDとは異なります。 先頭文字はユーザはU、グループはCトークルームはR。+英数32文字(16進数?)という書式になっていました。

Template Message

新機能のTemplate Messageは、トーク上に複数のボタンなどで構成されるテンプレートを表示し、ユーザの応答を得るための仕組みです。Buttons, Confirm, Carouselの3種類を利用できます。

現時点でいくつか制約があるようです。気づいたものが以下。

  • iOS版のLINEアプリ(バージョン 6.6.2)では、Template Messageは「LINEアプリのバージョンが古いため(ry」と表示され、利用できません
    • バージョン 6.7.0で対応されました
  • Android版のLINEアプリ(バージョン 6.7.0)では利用できました
    • バージョン 6.5.1では「ご利用のバージョンでは対応していないか不正なURLです」と表示され、利用できませんでした
  • ButtonsおよびCarouselにおいて、APIリファレンスにはthumbnailImageUrlが省略可能と書かれていますが、省略するとエラー*3が返ります*4
  • [11/2追記]Template Messageに設定するaltTextは「非対応端末で表示される代替テキスト」と説明されていますが、Notificationおよびトーク一覧画面での表示にも使われています(クライアントバージョン 6.8.0時点)

グループおよびトークルーム

従来のBOTはユーザと1:1のトークしかできませんでしたが、Messaging APIでは、グループおよびトークルームへの招待が可能となりました。

こちらも現時点でいくつか制約があるようです。気づいたものが以下。

  • グループに複数のBOTを招待できない。2つ目以降のBOTは「招待中」のまま保留され、Webhookのjoinも飛ばない
  • トークルームは、招待したタイミングではjoinは飛ばない。招待後、最初のメッセージがWebhookで送られる直前にjoinが投げられる
  • トークルームにも複数のBOTを招待できない。2つ目のBOTを招待すると、即退出される(元々いたBOTが退出するケース、2つ目が退出するケースの両方を観測)
  • トークルームのメンバーはキックできない仕様なので、BOTは自らLeaveAPIで退出しない限りルームに一人取り残される。ルームの参加人数を知ることもできないので、取り残されたら抜ける手段はなさそう
  • グループおよびルームからのWebhookでは、そのメッセージがどのユーザの発言かを知ることはできない(送信元はGroupIdおよびRoomIdとなる)。Template MessageからのPostback Eventも同様。
  • ユーザと1:1のトークでは、ユーザの表示名などはGet ProfileAPIで取得できるが、グループおよびトークルームのメッセージ送信元(GroupIdおよびRoomId)の情報を得ることはできない
  • BOTの送信したメッセージは、BOTには送信されることはない(オウム返しするBOTで、無限ループを気にする必要はない)
  • [10/30追記]すでにBOTがグループに所属している状態で他のユーザを招待すると、再びWebhookでjoinイベントが送信される。タイミングは「参加」時ではなく「招待」時点。意図がわからない。

参考

*1:参加したわけでなく、LINE LIVEで見てました。Beacon欲しかった…

*2:Tokenは、横の[ISSUE]ボタンをクリックすると発行されます

*3:linebot: APIError 400 A message (messages[0]) in the request body is invalid

*4:SDKでなくLINEのバックエンドの仕様のようですが、どこに申告すればいいのでしょうか?

調整さんリマインダLINE BOTを作ってみた

調整さん」を(日程調整ではなく)出欠の管理に使っている前提で、出欠登録のリマインダをLINE BOTとして作ってみたので、そのメモ。

[9/29追記]LINE BOT APIはDeprecatedとなり、Messaging APIに置き換わりました。同時にSDKもMessaging API対応のものに更新されていますので、ご注意ください。

[10/23追記]LINE BOT API Trial Accountは、11/16に完全削除を予定されていると発表されました。

新しいMessaging APIへの置き換えについては、次のエントリを参照してください。

nowsprinting.hatenablog.com

環境

仕様

  • 調整さんのイベントをクロールし、3日後に開催される予定がある場合、参加人数などを通知する
  • 通知は、BOTと友だち登録した人全員に送られる(cronで毎朝8:00に起動)
    • LINEグループに参加させたかったが、BOTはLINEグループに参加させることはできない*1
    • BOTの友だち登録は、デフォルトでは50人まで
    • BOTはid検索できないため、友だち登録は次の手順で行なう:QRコード画像を送る→画像を端末に保存→LINEで[友だち追加]→[QRコード]→[ライブラリ]→保存したQRコード画像を選択
  • 監視対象の調整さんイベントは、ハッシュをapp.yamlに記述しておく
  • LINE BOTのシークレットキーなどもapp.yamlに記述

GAE/Goのプロジェクト作成

Google Cloud Platformコンソールで新規プロジェクトを作成。プロジェクトIDを確定させる。

LINE BOT(チャンネル)の開設・設定

  • BOT API Trial Accountを開設
  • 開設したチャンネルの"Basic Information"にある"Callback URL"に、AppEngine側のコールバックを受け取るURLを設定
    • "https:// YOUR_PROJECT_ID .appspot.com:443/line/callback"
  • "Server IP Whitelist"は設定しない。設定するとAppEngineのIPアドレス変動に対応する必要があるため

必要なパッケージのインストール

$ goapp get github.com/line/line-bot-sdk-go/linebot
$ goapp get golang.org/x/text/encoding/japanese
$ goapp get golang.org/x/text/transform

LINE BOTの基本部分を実装

LINEからのコールバック用のハンドラをfunc init()に定義する。パスは上で定義したもの。

func init() {
    http.HandleFunc("/line/callback", lineCallback)
}

lineCallback()の中身は、ほぼBOT SDKのサンプルをコピペ。ただし、App Engineではhttp.Clientとしてappengine/urlfetchを使用する必要があるため、LINE BOT Clientからもこれを使うように、下記のように初期化する。

bot, err := linebot.NewClient(channelID, channelSecret, channelMID,
                        linebot.WithHTTPClient(urlfetch.Client(c)))

ここで一度goapp deployして動作確認。BOTを友達登録して、トークで送った内容をオウム返ししてくれればok.

友だち登録時にMIDを取得してデータストアに格納する

LINEへのメッセージ送信には、toにMID(LINEユーザの固有ID)を指定する必要がある。友だちに一斉配信するためには、あらかじめ友だち登録時に相手のMIDを取得しデータストアに保存する。

友だち登録および解除は、メッセージとは別フォーマットのオペレーション・イベントが通知される。パラメタのOpTypeによって、追加(ブロック解除も等価)、削除を判別できる。

なお、メッセージとは送信者のMIDが格納されている位置が異なる。OperationContent.Params[0]に入っているので注意。

    if content.IsOperation {
        //オペレーションイベント受信
        opContent, err := content.OperationContent()
        if content.OpType == linebot.OpTypeAddedAsFriend {
            task := taskqueue.NewPOSTTask("/task/addfriend", url.Values{
                "mid": {opContent.Params[0]},
            })
            taskqueue.Add(c, task, "default")
        } else if content.OpType == linebot.OpTypeBlocked {
            task := taskqueue.NewPOSTTask("/task/removefriend", url.Values{
                "mid": {opContent.Params[0]},
            })
            taskqueue.Add(c, task, "default")
        }
    } else if content.IsMessage && content.ContentType == linebot.ContentTypeText {
        //テキストメッセージ受信
        (snip)
    }

Task Queue/task/addfriendは送信者のMIDをデータストアに保存、/task/removefriendは送信者のMIDをデータストアから削除するもの。

メッセージを全員に送信

テキストメッセージを受信したとき処理を、送信者にオウム返しするもの(サンプルの実装)から、データストアに保存されている全MIDへの送信に変更する。

//データストアから購読者のMIDを取得
q := datastore.NewQuery("Subscriber")
var subscribers []Subscriber
if _, err := q.GetAll(c, &subscribers); err != nil {
    return err
}
mids := make([]string, len(subscribers))
for i, current := range subscribers {
    mids[i] = current.MID
}

//全員に送信
bot.SendText(mids, message)

調整さんリマインダ処理

調整さんリマインダは、下記処理をcronで毎朝起動されるようにする。

調整さんのイベントをクロール

cronからキックされるハンドラをfunc init()に追加する。

http.HandleFunc("/cron/crawlchouseisan", crawlChouseisan)

以下、crawlChouseisan()の実装。

調整さんのAPIは公開されていないが、イベント作成者にのみ表示される「出欠表をダウンロード」リンクのURLから、csv形式のレスポンスが得られるのでこれを利用する。

調整さんのイベントハッシュは、イベント作成時にコピーしておき、app.yamlに定義したものを取得する。

url := "https://chouseisan.com/schedule/List/createCsv?h=" + os.Getenv("CHOUSEISAN_EVENT_HASH")
c := appengine.NewContext(r)
client := urlfetch.Client(c)
res, err := client.Get(url)

csvをパースする。MS932なのでjapanese.ShiftJISデコーダを使用している。

また、encoding/csvReaderは、1レコード目と異なるカラム数のレコードをエラーと判断する。調整さんのcsvは1レコード目がタイトルのみとなっており、素直に読むとデータ行で"wrong number of fields in line"というエラーメッセージが出てしまうので、下記にようにErrFieldCountは無視するようにした。

reader := csv.NewReader(transform.NewReader(csvBody, japanese.ShiftJIS.NewDecoder()))
for {
    row, err := reader.Read()
    if err == io.EOF {
        break
    } else if e2, ok := err.(*csv.ParseError); ok && e2.Err == csv.ErrFieldCount {
        //フィールド数エラーは無視
    } else if err != nil {
        log.Errorf(c, "Read chouseisan's csv failed. err: %v", err)
        return nil
    }
    (snip)

日付などのパース処理は割愛。日付をキーにしたMapに詰める。

3日後の予定があれば通知

AddDate()で3日後を指すDateを作り、Mapにその日のValueが存在すれば通知対象とする。なお、App Engine実行環境ではUTCが使われるため、JSTを明示的に使用する。

tz, _ := time.LoadLocation("Asia/Tokyo")
today := time.Now().In(tz)
(snip)

//3日後の予定をピック
after3days := time.Date(today.Year(), today.Month(), today.Day(), 0, 0, 0, 0, tz).AddDate(0, 0, 3)
obj, exist := m[after3days.String()]
obj, exist := m[after3days]
if !exist {
    log.Infof(c, "Not found schedule at 3 days after.")
    return
}

//メッセージを組み立てて送信
(snip)

cron.yamlを作成

作成したハンドラを、毎朝8:00 JSTに起動されるようにcronを定義。

cron:
- description: crawl chouseisan every day
  url: /cron/crawlchouseisan
  schedule: every day 08:00
  timezone: Asia/Tokyo

以上をデプロイして、今のところ想定通り動作している模様。

途中、シネマカリテで『不思議惑星キン・ザ・ザ』<デジタル・リマスター版>を観た影響で、ひたすら「クー」と返す "キン・ザ・ザBOT" に作り変えたくなったけど、よく我慢した。

ソース

下記リポジトリで公開しています(Apache License 2.0)

GitHub - nowsprinting/ChouseisanReminder: Reminder line-bot for chouseisan.com

はじめてのGoでありAppEngine/Goアプリなので、作法とかなっとらん自覚はあります。なので参考程度に。『みんなのGo言語』ポチったので届いたら読んで改善したい。

参考

*1:りんなはグループに登録できるので、トライアルの仕様?

Unite 2016 Tokyo に行ってきました #unite2016tokyo

Unite 2016 Tokyoに行ってきました。去年はVR系セッション優先で聴講しましたが、今年はVR、最適化、絵づくりあたりを満遍なく。

講演資料は下記ページで即日公開、動画も近々公開されるということで、印象深かったメモのみ。

Unity - Unite 2016 Tokyo 講演ガイド

DAY 0

トレーニングデイのコース A「Unity サービス実装ワークショップ」に参加してきました。Unityのクラウドサービスである Cloud Build, Analytics, In-App Purchase, Adsなどの体験ワークショップ。

Cloud Build以外は使っていなかったのですが、AnalyticsとGame Performance*1は適用も簡単で、早速自社プロダクトに組み込んでFabric*2と比べてみようかと。

このワークショップでは、教材のプロジェクトだけでなく、UnityのインストーラWin32/64/MacOSX)まで入ったUSBを配布され、プロジェクトはごく最小限のエディタ操作とコピペで動作、さらにはCloud Buildの体験にはGitHub上のプロジェクトをforkするだけと、かなり至れり尽くせりの(安全サイドに倒した)ワークショップでした。

この手のワークショップ、これまで主催側として上手く行った例がないのですが、やはりこれくらい準備しないとダメですね。見習いたい。

DAY 1

モバイル端末向けのUnityアプリケーションの最適化実践テクニック

  • 最適化の前に、良いデータを計測することが大事。各種プロファイリングツールの紹介、使いかた
  • テクスチャ、モデル、オーディオの最適化
  • メモリのフラグメンテーションに注意する*3
    • boxing, foreach*4, 文字列操作に注意
    • JSON操作は、Unity 5.3以降ならJsonUtilityを使うこと
  • PrefabやCanvasを適切に分割する

実践!Oculus Rift - VR開発テクニック

  • Oculus RiftおよびGear VR向けアプリをストアに掲載する申請ができる
  • バイナリは、Store/RC/Beta/Alphaの4種類(4フェーズ)アップロードできる
  • カテゴリにある「先行アクセス」はBeta版などに使える。「コンセプト」はフルゲームでないアプリでも申請可能。
  • 必要なストア画像などはOculus Store Art Guidelineを参照
  • 価格もOculusで審査される。法外な値段はリジェクトされる
  • VR酔いの程度を三段階にレーティングされる

ハードウェア性能を引き出して60fpsを実現するプログラミング・テクニック

DAY 2

ホンモノ志向のVR空間づくり

  • 3Dをどう見ているか
  • リアル世界で生活して経験を積んでいるので、写真でも立体や遠近感を得られている
    • 知っているものがあるとスケールがわかり、距離もわかる。道路標識とか
    • 線で描かれた立方体を、立方体だとわかる
    • この感覚は、13歳くらいで習得できるらしい
  • 静止画ではわかりにくくても、動いているとわかる
  • Oculus Frameworkの"Teleportation"は参考になる
  • 黄金比、フィボナッチ、Rule of 3rd。科学的根拠はない
  • Light/Shadow
  • VRコンテンツを作るとき、単眼でテストする。それで十分立体感を得られるならば、二眼にすればよりよく見えるはず

映像制作のゲームチェンジャー:メイキング オブ ”THE GIFT”

  • カラーボールのシーンは、頂点数がIntegerの最大値を超えてマイナス表記になった
  • ボールは複数あわせて1メッシュにしている
  • ボールのポリゴンは実は粗い。少し小さめの円形に切り取るシェーダで球体に見せている
  • デザイナさんがエディタで作業できるレベルで動く

marza-realtime.com

Fate/Grand Orderにおける、ディライトワークス流Unity活用術

  • バトルキャラはビルボード。槍など横に振り回すもの(奥行き表現があるもの)は3D。Mayaで作ってfbxエクスポート。
  • エフェクトなどMayaでは表現が難しい物はUnity側でエフェクトをPrefabにしてぶら下げる
  • モーションはPlayMaker。カメラ移動、エフェクトの再生もここで。キャラ・モーションが増えたときもActionの追加で済ませる(アプリ更新を避けられる)
  • 宝具(必殺技)演出はuSequencer(カットシーンエディタ)で作成
  • ゲーム開発環境とは別環境の「宝具制作環境」。実行ボタンを押すと宝具が再生される。デザイナが素早く開発できる
  • Unityエンジニア=プランナーでもデザイナーでもプログラマでもない。今後増えてくる役割かも?

マルチシーン編集の使い方

  • Unity 5.3から
  • シーンを並べるほか、レイヤのような使いかたで編集の競合を避けられる
  • ヒエラルキにシーンをD&Dすれば追加で開く
  • 新しいSceneManeger API

Unityとアセットツールで学ぶ「絵づくり」の基礎(ライト、シェーダー、イメージエフェクト)

  • Unity 4までの絵づくり、Unity 5からの絵づくり
  • ぼくのUnityと違う
  • 「色を塗る」のではなく、「光を反射させて色を出す」。要素は、マテリアル×ライト×カメラ
  • 間接光

Making of The Modern Zombie Taxi Co.

  • オブジェクトの選択は、Look at Button (Gazing) が最適
  • 入口をリビングルームにした。プレイヤーをVRに慣れさせる空間
  • ダッシュボードに表示する情報は、通常のゲームより少なくしないとノイズにしかならない。当初11項目を2項目に減らした
  • 道案内も工夫。VR空間内を見てもらえるように、オブジェクトとして配置。
  • チュートリアルも難しい
    • VR経験のない人は、VR世界に興奮する。ゲームを遊ぶことを忘れる。
    • シンプルに、最低限に、同じ情報を繰り返し提示する。
  • ほか、VRでやってはダメなことも試してみた結果なども紹介されていて面白い

所感

トレーニングデイも、懇親会も、そして講演も、楽しめました。 今年から導入された有料S席システムも利用しましたが、聞きたい講演を確実に、しかも並ばず入れるのは快適。

最近よく言われる、講演中のシャッター音問題も、各回開始前アナウンスの徹底の効果なのか、スライド全ページ撮影するような人は見当たらず。ほかのカンファレンスもこうなるといいですね。

関係者、登壇者の方々、ありがとうございました!

*1:クラッシュログを採取してくれるサービス。BetaなのでProライセンスが必要

*2:先月Unity SDKがリリースされた。AnswersとCrashlyticsがそれぞれAnalyticsとGame Performanceに競合

*3:この話題は携帯電話Javaやってた頃を思い出す

*4:arrayをヒープにコピーしてしまう