DeNA Testing Blog

Make Testing Fun, Smart, and Delighting End-Users

タクシーアプリ「GO」Android版へ自動テストを導入するまでの道のり

こんにちは、Androidチームの田熊(fgfgtkm)と外山(sumio)です。SWETのAndroidチームでは、Androidのプロダクトに対して自動テストのサポートをしています。

この度株式会社Mobility Technologiesが提供するタクシーアプリ「GO」のAndroid版に対する、おおよそ2年間に渡る取り組みが終了しました。

本記事では、この取り組みで行ってきた次の2点を紹介したいと思います。

  • 「GO」のAndorid版に対してどのような自動テストを導入したか
  • 開発チームに自動テストを定着させるまでにやったこと

タクシーアプリ「GO」に対しての自動テスト導入

SWETのAndroidチームは「社内のAndroidエンジニアが自動テストを書くことを習慣化している」ことをゴールの1つとして、自動テストを普及させるための取り組みをしています。 その中でAndroidのテストナレッジの社内展開を目的としたハンズオンを実施してきました。

しかし、社内の多くのチームはテストが書きにくい設計になっているという課題を持っており、既存機能にテストを書くためには設計を改善する必要がありました。(参考:テストの社内普及のための取り組みとして、Androidテストハンズオンを実施しました

今回紹介する「GO」(旧サービス名:MOV)も例外ではありません。 ハンズオンではカバーしきれない現場の課題を解決して自動テスト導入を促進したい、そして「GO」をそのモデルケースにしたい、と考えたのが本取り組みのきっかけです。

それから約2年間、開発チームと協力しながら次の施策を実施しました。

  • コード改善 + ユニットテストの導入
  • スクリーンショットテスト

それぞれどのようなことをやってきたか、具体的にお話したいと思います。

コード改善 + ユニットテストの導入

当時の「GO」は歴史的経緯から一貫したアーキテクチャが定められておらず、多くの処理がFragmentやActivityに記述されていました。メイン機能となる地図を表示している画面(以降、地図Fragmentと呼びます)も同様で、配車に関連する複雑なロジックが地図Fragmentに実装されていました。

最初に行ったことは、この地図Fragmentに実装されたロジックを別クラスに切り出した上でユニットテストを書けるようにし、MVVMの構成にすることでした。以降、地図Fragmentや関連する画面クラスの改善を地道に進めていきました。

画面から切り出したほうが良い処理をピックアップし、画面からの分離⇔ユニットテストを実装する。そのサイクルを繰り返すことで、少しずつユニットテストが書けるコードを増やしていきました。 

こうしてテストコードが追加された結果、そのテストコードを参考に開発メンバー自身が自発的にユニットテストを実装してくれるようになりました。(具体的な数値については「自動テストを書くことを習慣化できたか?」のセクションで紹介します)

また、もともと開発チームでもMVVMアーキテクチャにしていきたいという思いがあったため、開発チーム主導でMVVM化がどんどん進んでいき、今ではほとんど全ての機能がMVVMアーキテクチャで実装されています。

改善した内容の一部はMOV Android版に対する「コード改善+テスト導入」の取り組みの紹介でもまとめていますので、是非ご参照ください。

スクリーンショットテスト

ユニットテスト導入をある程度進めたタイミングで、一度リリース前検証で出た不具合チケットを整理しました。その結果、画面まで結合させないと発見が難しい不具合、ViewModel以下のレイヤのユニットテストでは発見の難しい不具合が高い割合で起票されていることがわかりました。

Androidアプリ開発においてこのような不具合はつきもので、問題を早期発見するためにUIテストを導入できないか考えました。そしてUIテスト実装のアイディアについて開発チームと議論を重ねた結果、スクリーンショットを活用したテスト(以降、スクリーンショットテストと呼びます)を導入することで合意しました。

SWETではスクリーンショットテストを導入するために、主に次の2つのことを実施しました。

  • スクリーンショットテストを書きやすくするための基盤を整備する
  • スクリーンショット一覧レポートの確認や画像差分の比較ができるようにCI/CDを整備

スクリーンショットテストを書きやすくするための基盤

スクリーンショットテストは画面が任意の状態になるようにデータをセットし、さらに非同期処理やレンダリング完了まで待った上でスクリーンショットを撮る必要があります。ユニットテストと比較するとテストコード側でセットアップする必要のあることが多くなり、ボイラープレートが増えてしまいます。

我々はできるだけスクリーンショットテストを書く大変さを軽減したいと考え、書きやすくするための基盤の実装に着手しました。そして開発チームと一緒に様々なパターンのスクリーンショットテストを実装しながら、機能をブラッシュアップしていきました。

この基盤は次のことを実現しています。

  • Junit5のExtensionとして提供され、テストに適用するだけで「GO」で安定してスクリーンショットテストを取得するために必要なセットアップが完了する(非同期処理の待ち合わせ設定など)
  • Activity・Fragment・DialogFragmentそれぞれの起動を簡潔に書けるようにする手段の提供
  • 画面単位・View単位など様々なスクリーンショット取得方法の提供
  • テストでよく使用されるEspressoのコードを汎用化

基盤の実装の一部はGOGO Screenshot Test for AndroidとしてOSS化しています。是非ご参照ください。

CI/CDの整備

取得したスクリーンショットは一覧化したレポートを作成することで、エンジニア以外のメンバーでもデザインのレビューがしやすくなります。 また、reg-suitといったツールを使えば、Visual Regression Testにも活用できます。 例えば、前回リリース時点のスクリーンショットと現在のバージョンのスクリーンショットを比較すれば、意図せぬUIの変更が含まれていないか検証できます。

Andorid スクリーンショットテスト 3つのプロダクトに導入する中で倒してきた課題より引用)

これらのことを実現するために、次のようなワークフローをBitriseを使って整備しました。このワークフローはマージやタグ追加のタイミングで実行されます。

  • Instrumentation Testを実行し、スクリーンショットを取得する
  • 取得したスクリーンショットの一覧レポートを作成する
  • 任意のバージョン間のスクリーンショットを比較してreg-suitのレポートを作成する

あわせて、不必要な差分が含まれないスクリーンショットを安定して取得できるように基盤の改善をしました。

スクリーンショットテストの導入および、スクリーンショットを安定して取るためにやったことはAndorid スクリーンショットテスト 3つのプロダクトに導入する中で倒してきた課題にもまとめています。是非こちらもご参照ください。

自動テストを書くことを習慣化できたか?

冒頭でSWETのAndroidチームは「社内のAndroidエンジニアが自動テストを書くことを習慣化している」ことをゴールの1つとしていると述べました。 それでは自動テスト導入の取り組みの結果、「GO」の開発チームにおいて自動テストを書くことを習慣化できたのでしょうか。

最近のリリース時点での自動テスト件数とカバレッジの推移は次のようになっています。

まずはLocal Testの推移です。Local Testには主に画面以外のユニットテストが含まれます。

次に、スクリーンショットテストが含まれるInstrumentation Testの推移です。スクリーンショットテストを導入したバージョン5.3.0から集計しています。

ユニットテスト・スクリーンショットテストともに各バージョンで増加しています。また、これらのテストはほとんどSWETメンバーによる実装ではなく、開発メンバー自身が実装したものです。取り組みを始めた当初はユニットテストが十数件ほどでしたが、現在ではコンスタントに自動テストが増えています。

この結果から、「GO」の開発チームにおいては自動テストを書くことを習慣化できたと考えています。

自動テストの習慣化が実現できたのは、開発チームの尽力によるところが大きいです。一方でSWETからも自動テストが定着するような働きかけを行ってきました。

次のセクションでは、自動テストを定着させるためにどのような取り組みを行ってきたかを紹介します。

自動テストがチームに根付くまで

導入したテストが効果を発揮するためには、テストの継続的運用が不可欠です。 そのためには、開発チーム自身がテストの追加・修正といったメンテナンスをし続ける必要があります。

  • テストを導入したものの、導入した担当者が異動を機にメンテナンスされなくなってしまった
  • テストを書く気持ちはあるけれども、ついついプロダクトの実装を優先してしまう

といった話は色々なところで耳にします。 開発チームのメンバーひとりひとりがテストを書けるようになり、更にその状態を継続するのは大変なことです。

幸い、「GO」開発チームでは自動テストを書くことを習慣化できました。 自らテストを書く習慣は「こうすれば身に付く」といった答えのあるものではありませんが、一例として「GO」開発チームにおける習慣化のためにSWETが働きかけてきたことを紹介します。

ユニットテスト

「コード改善 + ユニットテストの導入」で触れたようなテスタビリティ改善の取り組みによって、開発メンバーの書いたテストが少しずつ増えるようになりました。

この段階になってから重要なのがトラブル発生時のサポートです。 特に次のようなトラブルは、一歩間違うと未解決のまま放置されてしまうことになりがちです。

  • モックライブラリMockkで意図通りにモックできず、テストが失敗してしまう
  • CI動かしたときだけ、時々テストが失敗してしまう

失敗したテストが残ったままの状態だと「テストをオールグリーンの状態に保つ」という気持ちが折れてしまい、テストがメンテナンスされず放置されるきっかけになってしまいます。 そうなってしまわないように、Slackに常駐して、テスト関連のトラブルに気付いたときには積極的にサポートするようにしました。

現在では、このようなトラブルも開発メンバー自身で解決し、SWETのサポート無しでテストの追加・修正を継続できています。

スクリーンショットテスト

一方スクリーンショットでは、先に触れたようなスクリーンショットテスト基盤やワークフローの整備だけでは、開発チームが自らテストを追加するまでには至りませんでした。 そこからテストを書く習慣が根付くまでの道のりは試行錯誤の連続でした。

モブプロでテストを書くハードルを下げる

当時は、新たに構築したテスト基盤の使い方とテストの書き方の説明会を実施し、その後のテストの追加は開発チームに任せようという考え方でいました。 ところが、説明会終了後もテストが増えないままだったため、テストを書くハードルをもっと下げる必要があるのではないか、と考えました。

そこで、更にテストを書くハードルを下げるために、とある画面のスクリーンショットを撮るテストをモブ・プログラミング(以降モブプロと表記します)で実装してみることにしました。
モブプロの役割分担は次の通りです。

  • ドライバー(プログラムを書く人):SWET
    • テスト基盤の使い方などを説明しながらコーディングしました
  • ナビゲーター(ドライバーに指示する人):開発メンバー
    • スクリーンショット対象画面を表示するために必要なデータのセットアップ方法を案内しました

このモブプロで、「シンプルなコードで目的の画面のスクリーンショットを撮る」方法が開発チームに伝わったという手応えがありました。

モブプロで開発メンバーにコーディングしてもらう

その後新機能のリリースが立て続けに発生し、長い間テストが追加されない状態でした。

大きなリリースが終わり、開発チームが一息ついた頃に「そろそろスクリーンショットテスト書いてみませんか」と呼び掛けてみたところ、 「前回のモブプロでは自分が手を動かさなかったので書き始められる自信がない」という意見を多数いただきました。

確かに、前回のモブプロでは主にSWETがドライバーをしていたため、開発メンバーがコードを書く機会はほとんど有りませんでした。 そこで、開発メンバーをドライバーに据えて、改めてモブプロを開催することにしました。

  • ドライバー:開発メンバー1名
  • ナビゲーター:SWETと、残りの開発メンバー
    • SWETはテスト基盤の使い方を案内しました
    • 残りの開発メンバーは、狙った画面を表示するための方法を案内しました

このモブプロではスクリーンショット撮影に必要な一通りのコードを全員に書いて欲しかったため、開発メンバー全員がドライバーを担当するまで毎週開催することにしました。 その甲斐あって、スクリーンショットテストが追加されるようになりました!
・・・が、しばらく経つとテストが追加されない状態に戻ってしまいました。

短期的なゴールを共有する

そうこうしている内に、私達が支援できる期限は残り3か月を切ってしまいました。 当初はテストで撮影したスクリーンショットをデザイナーさんにレビューしてもらう計画だったにもかかわらず、肝心のテストが増えないのでレビューしてもらう画像も無いという状況でした。

この状況を打開するため、開発チームに次のような提案をしてみました。

  • 次回リリースのデザインレビューでは、テストで撮ったスクリーンショットをデザイナーさんにレビューしてもらうようにしたい
  • デザインレビューまでに、次回リリースで追加・修正される画面だけで良いからスクリーンショットを撮れるようにしたい
  • スクリーンショットテストを1人で書くのはまだ不安があると思うので、引き続きモブプロで書けるだけ書いていきたい

有り難いことに、開発メンバーの皆さんに賛成してもらえたので、3巡目となるモププロを開催しました。 今回のモブプロは役割分担こそ2巡目と同じものの、次の点が異なっていました。

  • テスト完成の期限が明確になった
  • スクリーンショットを撮る対象画面の範囲が明確になった

期限と範囲が明確なため、モブプロだけではカバーしきれなかった画面のテストが次々と追加されていきました。 モブプロだけでは必要なスクリーンショットが撮れそうになかったため、デザインレビューの期限に間に合うよう頑張ってもらえたのだと思っています。

また、提案したときは意図していなかったのですが、対象に難易度の高い画面が含まれていた点が良い方向に作用しました。 モブプロで難易度の高い画面に取り組んだことで、意図通りの状態で対象画面(Fragment)を起動するスキルが格段に向上したのです。 合わせて、難易度の高い画面に対応するための改善を、テスト基盤に多数取り込むことができました。

スキルが向上してスクリーンショットテストを書く手間が減っていくと、デザインレビュー以外の方法でも便利に使えるという声が開発メンバーから聞かれるようになりました。

  • Pull Requestを出すときにスクリーンショットを貼り付けるのが楽になった
  • 複雑な操作をしないと出せない画面を確認するのが楽になった

最終的には、デザインレビュー当日に必要なスクリーンショットテストが全て揃い、色々な画面サイズ・解像度でのスクリーンショットをデザイナーさんに渡すことができました。 開発メンバーの皆さんには、私達が離脱した後も継続してスクリーンショットテストを活用してもらえています。

まとめ

「GO」のAndroid版にユニットテストとスクリーンショットテストを導入し、開発チームが自動テストを書く習慣が根付くまでに取り組んだ内容を紹介しました。

今回の取り組みを通して、次のような気付きを得ることができました。

  • ユニットテストのようなシンプルなテストは、最初に参考となるテストを追加することで他のメンバーも書き始めやすくなる
  • 一方で、スクリーンショットテストのような複雑なテストを書いてもらうには、基盤の導入だけでは不十分
  • テストの書き方を身につけてもらうには、当人がドライバーとなってモブプロを行うのが効果的
  • 目標や期限が決まるとモチベーションがアップする

その一方で、次のような幸運に恵まれた点も多かったと思います。

  • 開発メンバーの皆さんが、自発的に自動テスト実装やアーキテクチャの改善をしてくれたこと
  • 開発メンバーの皆さんが、デバッグが難しい複雑なトラブルでも解決できる高いスキルを持っていたこと
  • 開発メンバーの皆さんがいつも協力的だったこと
  • 私達の支援期間が終了するまでに何とかしたいという気持ちを全員が持っていたこと

他のチームで、ラッキーな面を差し引いたとしてもなおこの方法が通用するのかは分かりませんが、今回の経験で得られた学びはとても大きなものでした。 本記事が、皆さんがテスト導入に取り組むときのヒントになれば幸いです。

謝辞

Mobility Technologiesの「GO」Android版開発チームの皆さんには、本取り組みのために貴重な時間を割いていただきました。 また、UIテスト基盤のOSS化について快諾していただきました。改めて感謝いたします。