OpenSTFでAndroidのCIを2倍早くする

はじめまして!技術部モバイル基盤グループの加藤(@k0matatsu)です。
業務の一部でCIお兄さんとしてJenkins氏のメンテナンスなどを行っています。

今日はf:id:komatatsu:20160815174157j:plainf:id:komatatsu:20160815174215j:plainにする話をしたいと思います。
CI待ち時間1/2で PR/レビューのサイクルの速さ2倍(当社比)です。

※ ビルド所要時間のボトルネックは環境やジョブ内容によって異なるため効果には個人差があります。

当社のAndroid CI環境

さて、開発効率を2倍にする前に、まずは当社のCI環境がどうなっているか説明が必要ですね。
当社のAndroid向けCI環境は幾つかの試行錯誤を経て、現在はAmazon Web Service(AWS)を使って構成されています。
下図のように、Amazon EC2(EC2)インスタンス上に構成管理ツール:itamaeを使って作成されたJenkinsのmaster/slave構成を擁し、その中でAndroid向けSlaveが2台稼働しています。

f:id:komatatsu:20160815173846j:plain

Jenkins Slave上では、Instrumentation Testの実行にはAndroid Emulator PluginとARMエミュレーターを使いテストを実行しています。
EC2上ではハードウェア仮想化が出来ないためIntel x86 Emulation Acceleratorが利用できず、 またAndroid Emulator Pluginによるエミュレーターの起動にも時間が掛かっており、エミュレーターの起動とInstrumentation Testのエミュレーター上での実行時間がボトルネックになっていました。

現構成での解決策と問題点

そのため、今ではテストをSmall/Medium/Large/Enormousの4サイズに分け、そのサイズごとに実行タイミングを変えることで開発サイクルとCIの流れを最適化しました。
テストのサイズ分けについて、詳しくは先日のAndroid/iOSアプリのテストの区分戦略をご参照ください。
その中から、今回はCIに関係の深いSmall/Medium/Largeのみ言及します。

サイズ 実行されるタイミング 内容
Small プルリクエスト毎 JVM Test
Medium プルリクエストのmasterへのマージ時 Instrumentation Test
Large 任意 Espresso Test

テストサイズにより実施するテストを分けたことによってプルリクエスト時にはエミュレーターを起動しなくて良くなったため、テストにかかる時間が大幅に短縮されテスト以外の処理と合わせても5min/pr程度に収まる様になりました。

ですが、マージの度に実行するInstrumentation Testには今まで通りの時間がかかります。 このため、マージが頻繁に発生する場合はキューが詰まるという問題が発生し、Slaveを追加してジョブの並列度をあげることで全体のスループットをあげる方法(別名:札束で殴る)で解決せざるを得ない状況でした。
しかしこの方法は根本的なエミュレーターの準備と利用に時間がかかるという課題を解決できていないので1つのslaveによる実行時間が短くなることはなく、1回のビルドにかかる時間以上に開発効率が上がらないという限界があります。
この問題を根本から解決するにはARMエミュレーターを捨てるほかありませんでした。

OpenSTFの登場、そしてビルド所要時間1/2へ...

そんな折に燦然と姿を現したのはOpenSTFでした。 OpenSTFはブラウザからAndroidの実機を遠隔操作することが出来るオープンソースのツールです。 f:id:komatatsu:20160815173900j:plain

端末操作だけでなく、ログの取得やブラウザ経由のアプリのインストールなど様々な機能を持っており主に検証用途で利用されています。 当社でも試験的に導入しており、誰も使ってない検証用端末を繋いで簡単な動作確認などはOpenSTF上で出来るように環境構築していました。
OpenSTFの空き端末を使ってテストを実行出来るようになればこの問題を解決できるんじゃないですか?私はそう思いました。

OpenSTFでテストを走らせる

OpenSTFでテストを実行するためには、まず端末を利用状態にして確保する必要があります。 その後、確保した端末の接続用のIPとPortを取得し、adb connectする事でインターネット越しにテストを実行することが可能になります。 f:id:komatatsu:20160815173918j:plain

最新のversion2.0.1ではAPI機能が開放されており、APIを使って端末を利用状態にしたり、adb connectに必要な情報が取得出来るようになっています。

さっそく簡単なスクリプトを書いて挑みます。

DEVICE_SERIAL=`ruby stf.rb device | sed -e 's/"//g'`
DEVICE_INFO=`ruby stf.rb connect ${DEVICE_SERIAL} | sed -e 's/"//g'`
IP_PORT=(`echo $DEVICE_INFO | tr -s ':' ' '`)
export ADBHOST=${IP_PORT[0]}
export ANDROID_ADB_SERVER_PORT=${IP_PORT[1]}
adb tcpip ${IP_PORT[1]}
adb connect ${DEVICE_INFO}
adb devices

例に出てくるstf.rbはOpenSTFのAPIを叩くための簡単なRubyスクリプトです。APIドキュメントの一部をラップしています。
stf.rb deviceはAPIを叩いて端末を利用状態にし、その識別文字を取得する処理です。
同じく、stf.rb connectは上記の処理で取得した識別文字を使って接続に必要なIPとPortを取得します。

ネットワーク越しに端末に接続するadb connectを使うには、環境変数ADBHOSTANDROID_ADB_SERVER_PORTにそれぞれIPとPortを設定して置く必要があります。
その上で上記のようにadb tcpip {Port}でadb-serverのPortを設定し、adb connect {IP:Port}とすることで接続することができます。

また、初めて接続する際にはOpenSTF側にADBキーの設定が必要になります。 OpenSTFを開いた状態で上記のスクリプトを実行すると下図の様にADBキーの保存を促す画面が出ますので忘れずに保存しておきましょう。 f:id:komatatsu:20160815173602j:plain

ここまでで準備は完了です。
あとは思うがまま、Jenkins Slaveから./gradlew connectedAndroidTestしましょう!

まとめ

このように、OpenSTF連携を行うことでエミュレーターにより実行時間がかかる問題が解消し当社CIのビルド所要時間は半分になりました。
今回はエミュレーターと起動時間と、エミュレーター上でのInstrumentation Testの実行時間が主なボトルネックとなっていた為、エミュレーターを捨てOpenSTF上の実機を使うことで課題を解決しました。
同様にエミュレーターがボトルネックになっているケースでは有効な対策だと思います。

CIのメンテナはJenkinsおじさんなどと揶揄され、つらそうというイメージを持つ方も少なくないかもしれません。
ですが僕はCI待ち時間が半分になったのを確認した瞬間に最高の達成感を味わう事が出来ましたし、日々開発を頑張っている他のメンバーの待ち時間を減らすことが出来たのは誇りでもあります。
このように開発者の役にたっている事が実感できる基盤業務に興味がある仲間を募集しています。
めざせ、5min切り!
ありがとうございました。

謝辞

OpenSTFの開発は以下のお三方とコントリビューターの方々によって支えられています。
sorccuさん
guntaさん
vbanthiaさん
コントリビューターの皆さん
ありがとうございました。

Android/iOSアプリのテストの区分戦略

技術部の松尾(@Kazu_cocoa)です。

クックパッドのモバイルアプリ開発では、どのようなテストを書き、どのようなタイミングで、どのようなテストを実施するか?に関してエンジニア各位が意識を合わせるためにテストサイズを定義し運用してきました。ここでは、そんなテストサイズに関して簡単ですがまとめておこうと思います。

テストサイズとは

ソフトウェアテストに関わったことがある方なら テストレベル という言葉には出会ったことがあるかと思います。JSTQBでは、このテストレベルは"管理していくテストの活動のグループ"と定義しています*1。 そうでない方も、俗に言う単体テスト/統合テストなど聞いたことがあるかと思いますが、その区分がここで示しているテストレベルとなります。

一方、このテストレベルはV字型と言われる開発工程と合わせて世の中で広く使われているため、社内における共通認識を構築するにあたり個々人の認識を合わせる上で手間がありました。そこで、 私たちの開発スタイルに合うようにいくつかの指標を定義し、そこに沿ってテストを区分する方法をとりました。その結果、その区分に対して妥当な表現としてテストサイズ、という言葉をとりました。この区分はGoogleのTest Sizeの影響も受けています。

なお、テストレベルに関しては安定したリリースを継続するためのテストとテストレベルの話として記事を書いたことがあります。当時はまだこのサイズに対する定義も試行錯誤の段階だったのでサイズという表現を用いてませんでした。

以下では、今回の話の対象と区分、その実例に軽く触れながらどのような定義を用いているのか載せていきます。

対象

  • モバイルクライアントアプリ
    • サーバ側は対象外

テストサイズと区分け表

テストサイズをsmall/medium/large/Enormousに区分します。その中で、いくつかの依存する要素に対してどのような関係を持っているのかをまとめます。

feature Small Medium Large Enormous
network no localhost localhost/yes yes
system access no partial/yes yes yes
OS(APIs) no yes yes yes
View no partial yes yes
external system no no no yes
time per a test < 100ms < 2s < 120s < 500s
  • yes: 基本、mock/stubしない
  • no: mock/stubする
  • partial: テストしたい事を安定させるためのmock/stubはOKだが、基本はしない
  • localhost: localhostに向けた通信

基準

テストサイズを区分する上で、以下の要素を考慮に入れています。

  • 依存性 : OSへの依存、ライブラリへの依存など。依存性が高いほど、複雑な系を対象にする。
  • 実行時間 : テストの実行時間
  • 実施環境: ネットワークアクセスなどなどのテスト時に依存する周辺環境

これらは私たちの開発スタイルであったり、テスト容易性を考える上で特に重要な性質を持ってきています。

区分け表で使っている要素

  • network connection
    • モバイルアプリの通信環境
    • API Clientを置き換えるか、localhostを起動するか、など
  • system access
    • DB/Shared preferenceなど、データの書き込みがあるかどうかなど
  • OS APIs
    • OS固有のシステムへのアクセスがあるかどうか
  • View
    • Viewが実際に描画されるかどうか、など
  • External system
    • アプリ間連携とか含む
    • Pushとその模倣など

各種内容

実際にS/M/L/Eの4つのサイズに区分した例を出しました。その中で、それらを実際の業務でどのように使い分けているのかを例も交えながら取り上げていきます。

Small Test

内容

  • RUN FAST!!

実施頻度

  • PR毎

検出したいこと

  • 小さなロジック単位で、意図した通りにメソッドが動作さすること

検出しないこと

  • Viewが正しく表示されるか
  • ネットワークなどの周辺環境の変化に対する確認はできない

  • RoboletricやJVM上で実施されるテストコード
    • 社内では、基本的に何もアノテーションをつけない場合このテストサイズとしてテストが実行されます
  • XCTest/OCMock系を使ったテストコード

Medium Test

内容

  • OS提供のAPIや機能、描画へ依存するが一定のシナリオ実施までは行わないテスト
    • Intentの受け渡しによりViewが正しく起動するとか
    • ActivityやCustom Viewの単純な表示確認とか
    • 通信は基本mock/stubサーバ、localhostへの接続

実施頻度

  • PR毎、もしくはmasterにマージされる毎

検出したいこと

  • 幾つかの要素に対して依存性を持った要素の、主にロジック面で不備がないか
  • Androidに依存する箇所の動作を確認できる
    • intentの受け渡しが正しくできているか
    • 特定のviewの表示要素がクラッシュなく表示されているか
    • sharedpreferenceなどへの書き込みが正しくできているか
    • RoboletricでShadowにされているところ全般
  • iOSへの依存に関しても同様

検出しないこと

  • 画面遷移含んだ複数Viewの遷移が正しくできていること
  • 通信のタイムアウトエラーなどの、OSの外部環境に依存する振る舞いの変化など

  • AndroidTest、Espressoを使ったテストコード
    • @MediumTestアノテーションを付与する
  • XCTestを使ったテストコード

Large Test

内容

  • 一定のシナリオ(画面遷移)が期待通り実施されるか確認
  • その時の通信などの処理も確認

実行頻度

  • 任意、もしくはリリース毎

検出したいこと

  • Viewからの、一連の操作に対する内部処理が正しく動作しているかを確認する

検出しないこと

  • テスト対象となる単アプリ以外を含めた連携の確認
  • 実際の通信を行った上での、タイムアウトエラーなどによる表示の変化

  • EspressoやUIAutomator v2を使い、ある一定の操作を実際の描画含めて行うテストコード
    • @LargeTestアノテーションを付与する
    • 別途、社内では、Android TVに対する画面遷移含むテストの独自アノテーションを定義し実施しています
  • XCUITest / UIAutomation / EarlGrey
    • EarlGrey用のスキーマを用意
  • Appium(一部)

Enormous Test

内容

  • リリースする成果物や、アプリ関連携が正しく動作するか確認する
  • ある一定のシナリオを、リリースと同等の状態で実施する
  • OS側の設定を変化させた時のアプリの動作を確認する

実行頻度

  • 任意、もしくはリリース毎

検出したいこと

  • 複数のアプリを跨いでアプリが期待した通りに動作するか
  • システム設定に依存した箇所の表示の変化
  • リリース物そのものに対する画面網羅、画像取得、その差分検出して問題ないかどうか
  • 性能劣化がないか
  • 多機種試験

不可能

  • 表示の網羅

  • Appium
  • 手動テスト

Tips

テストサイズが小さいうちはDIなどの技術を用いてテスト容易性を上げる、依存性を下げるなどするかと思います。一方で、テストサイズが大きくなるとテストが不安定になる要素や私たちアプリ固有の依存を持つ箇所が目立つようになります。また、動きを観測することで数値計測が必要になることも多々あります。その場合に対して、私たちは必要な補助ツールを用意し、適宜対応しています。その一例を載せています。

直接テスト実施には関係ありませんが、テストをするために必要な情報を取得したり、忘れがちなところを補助するツールも作り実行頻度を調整して適用しています。

  • wda_client(Ruby)
    • WebDriverAgentを使う最小限のラッパー
  • license-tools-plugin(Java)
    • Androidアプリのライセンス作成を楽にするOSS

最後に

安定した開発サイクルを保ちサービス開発に集中できるようにするために、安定した・必要な時間内に終えることができるテストの実現は現在まだ必要とされる環境です。そのためには、開発のボトルネックとなっている箇所を様々な技術(skill, technology, knowledge)を用いて対応していくことが必要です。

弊社ではこのような取り組みを共に実施し、より良いサービスを提供し続ける為にエンジニアを募集しています。ご興味のある方は、是非とも覗いてみてください。

海外のユーザーを向いたプロダクト開発の工夫

こんにちは、検索事業部の岡根谷です。クックパッドは日本だけでなく海外でもレシピサービスを展開しています。今回は自分がプロダクトオーナーとして行った「関連レシピ」の複数言語同時リリースの話をしたいと思います。

関連レシピとは?

レシピページの下にあり、ユーザーの行動から関心を汲んで自分にぴったりなレシピを見つけるのを助ける機能です。 「パスタ」で検索してジェノベーゼパスタに興味を持ってレシピを開いた人が、より自分にあったジェノベーゼパスタを見つけることができる、というようなシナリオを想定しており、たくさんのレシピを闇雲に探索するのではなく絞り込む方向に導くことで「作る」という行動に繋げたいという思いがあります。

チームメンバーは世界中に散らばっており、各国のエンジニアやコミュニティマネージャーと連携して、スペイン語、アラビア語、インドネシア語、英語、ベトナム語など複数の言語のAndroidアプリで同時リリースを目指しました。

f:id:m_okaneya:20160811074222p:plain

アラビア語版関連レシピ

言葉がわからない国のプロダクト開発をどう進めるのか

プロダクトオーナーとして自分が重要な判断を行えるためには、誰よりも詳しく正しく、そこに暮らすユーザーの困っていることとプロダクトの提供価値を語れる必要があります。

会ったこともない国のユーザーのことを現地の人以上に正しく理解できるのかと怯みますが、その自信が持てないと、現地メンバーの一つ一つの意見に左右されてプロダクトの方向性を見失ってしまいます。 現地感覚を持つメンバーの意見は大事ですが、それを鵜呑みにするのではなく最終的な決定は自分が自信と責任を持って行えるために、様々な方法を試みました。

1. わからなくてもとにかく自分自身が徹底的に使う

地味ですが、とにかく徹底的に使って自分自身が一番うるさいユーザーになる努力は絶対に必要だったと感じています。開発中のアプリを自分のスマホに入れて、仕事中にかぎらず通勤中の電車でも布団の中でもとにかく毎日徹底的に使い続けるうちに、ユーザーが一体どういう場面でこの機能に出会うとうれしいのかという大きなストーリーとともに、アラビア語であってもよいケースそうでないケースの感覚が持てるようになりました。

とはいえ文字情報の理解はさすがにきびしかったので、スマホではテキスト選択して自動翻訳できる Google Translateを、PCのChromeのブラウザでは語意までわかる Google Dictionaryの力を借りました。細かいことですが、ページ全体を翻訳するのではなくわからない部分だけを選択してツールで訳すことで、なるべくユーザーに近い画面を見ることを心がけました。

誰よりもプロダクトに詳しくなるために、自分にできる方法でとにかくプロダクトと親密になることが重要だと実感しました。

f:id:m_okaneya:20160812083505p:plain

2. プロダクトの提供価値を一枚の絵で示し、みんなの共通理解にする

距離的に離れたグローバルの開発では、そもそもこのプロダクトは何を目指すものなのなんだっけということすら目線がずれてしまいがちです。「関連レシピは、言葉に現れないユーザーの関心を汲んでぴったりなレシピを提案する絞り込みの機能である」という定義が全メンバーの共通の理解になるよう、ユーザーストーリーを一枚の図に表現して、hangoutを通じて各国のコミュニティマネージャーにプレゼンをしたり、各国の開発者が見る社内ブログにのせたりして、浸透に努めました。

プロダクトの提供価値がいつでも誰でも参照できる伝わりやすい形になっていることで、それに基づいた公正な判断ができるようになります。この画が示せるようになったことで、ユーザーに興味を持ってもらえるよう広く多様なレシピを出したい、いう中東地域からの提案があったときも、検索を広げる方向はこの機能の役割とずれるから別の方法で実現しようと一緒に納得して決めることができました。

f:id:m_okaneya:20160810210742p:plain

3. 現地のコミュニティマネージャーにチェックしてもらって判断する

自分自身で使い込むことも大事ですが、ネイティブの目で見てより高い品質を確保するために、リリース判断には各国のコミュニティマネージャーからフィードバックを得て判断する、という方法を取りました。 自由回答にならないよう、実際のユーザーに近い状況でのフィードバックが得られるように対象レシピと観点を細かに指定し、スプレッドシートで作業シートの形式にして依頼しました。

f:id:m_okaneya:20160810185311p:plain

この方法のよかったところは、作業シートの形式にすることで主観的なフィードバックが定量化できるようになり、「何割以上◎だったらリリースする」という統一の基準を設けてすべての言語のリリース判断や比較が行えたことです。一方で限界は、プロダクトの目的を丁寧に繰り返し伝えてやってほしいことを相当に細かく説明しないと、実際に使うユーザーに近い正直な感想は得られないということです。関連レシピという名前が先行して「自分が作りたいレシピが見つかるか」ではなく「似ているかどうか」に終始したフィードバックが多くなってしまったのは反省点です。 考えてみれば、日本の開発をしていても、正直なユーザーの視点を持つのは非常に難しいことです。なので、国を超えて他人にそれをやってもらうことの難しさは一際です。

質問の仕方は工夫の余地があると思いますが、作業を明確にすることで定性的な感覚を定量化する、というのはよかったです。

4. ヘビーユーザーの行動を追跡する

プロダクトリリース後は、この機能が一体どのような場面で頼られて使われているのかを理解するために、それを最もヘビーに使ってくれているユーザーの行動を追跡するということをやりました。この方法のよいところは、実際のユーザーの行動という絶対的な証拠に基づいて、各国のメンバーと話ができることです。「ユーザーはきっとこう振る舞う」「これはユーザーにとってこうにちがいない」という主観を排して客観的な理解が持てるので、同じ土台の上に立ってみんなでプロダクトをよりよくしていくための議論ができます。

f:id:m_okaneya:20160810210656p:plain

実際のユーザーの中でも特に高頻度に使ってくれている人たちの行動は、一人のスタッフの意見よりもCTR等の数字より何百倍も説得力があります。チーム全員の目線が揃うと同時に、自分自身のユーザー理解が進むという意味でも非常によかったです。分析する中で、通常の検索の場面だけでなく、自分が気に入って保存したレシピから似たような見た目や盛り付けのレシピを見つける時に便利に使われていることがわかりました。

このことから、「言葉にしにくい希望を汲んで」という仮説とプロダクトの実態は一致しているけれど、当初想定した今日の食事決めの場面よりも、お菓子作りやパーティーのような計画を必要とする場面でより便利に使われているという事実がわかりました。

5. 日本語でやってみる

自分の言語だったら実感が持てるのではないか...と考え、日本のクックパッドのデータで関連レシピを再現してみるというのもやってみました。自分の言葉で普段食べるもので確かめることで、自分自身の実感は深まりました。しかし、これは結局自分の言葉か相手の言葉かという立場を逆転させただけなので、各国のメンバーと共有することはできません。国を超えた開発をするには、いつまでも自分の土俵に引きこもうとするのではなく、どんな土俵であってもみんなが対等に力を発揮できるような土台工事をできるようになるのが重要だと学びました。

f:id:m_okaneya:20160810211732p:plain

※未公開のテストプロダクトです

まとめ

振り返ってみると、結局大事なことや苦労したことは日本のプロダクト開発と共通だということに気づきます。

ちがう言葉だからわからないということはなく、自分自身が徹底的に使い込み、チームメンバー全員でプロダクトの目的を共有し、そして一人ひとりのユーザーの行動に向き合うことで、プロダクトオーナーとして責任を持って判断ができるようになります。

そしてその中で気がついたのは、日々料理をする人が困ることやうれしいと思うことは、言葉や文化が違っても本質的には同じであるということです。言葉や文化の振れ幅を排除して普遍的なものに目を向けることで、日本のユーザーにも世界中のユーザーにも頼られる、毎日の料理を楽しくするサービスになれるのではないかと信じています。

最後に

世界にサービスを展開を進めているクックパッドでは、世界中の毎日の料理を楽しみにすることを目指して一緒に働く仲間を募集しています。興味を持った方は こちらへ!