LoginSignup
22
11

More than 3 years have passed since last update.

今更Build Testable Apps for Androidを見て驚いたところメモ

Last updated at Posted at 2020-11-13

概要

去年のIOで少し古い動画なのですが、すごくいい動画なので、見てみてください。
特に驚いたポイントを太字にしています。
かなり端折っているのでおかしいなって思ったら動画の方を確認してください。解釈が違いそうっていう所があればコメントか修正リクエストをください :bow:
https://www.youtube.com/watch?v=VJi2vmaQe6w

テストにおいて、キーとなるアトリビュートはスコープ

スコープは一つのメソッドでテストがアプリケーションのどのぐらいカバーしているのか、または複数の機能、複数の画面にまたがることはできる。
スコープはテストの他の2つのアトリビュートに影響を与える。
→ スピード
ミリ秒単位で終わるものもあれば何分もかかるものもある。
Fidelity(忠実さ)
テストケースのシミュレートが、どのぐらい本当の世界のシナリオに近いか。スコープを広げれば広げるほど、テストのFidelityは上がる。しかし、逆にスピードを遅くする。一つのテストではすべてにおいて一番いいものはできない。

テストのバランスを見るにはテストピラミッドを見ると良い。

テストピラミッドは
上に行くほどFidelity(忠実さ)が高く、スコープが広がる、しかし、スピードやフォーカスやスケーラビリティがない。
https://developer.android.com/training/testing/fundamentals?hl=ja より
image.png

テストピラミッドとアプリの設計

何も設計しない場合

何も考えずにコードを書いていくと密結合になっていって、テストすることが本当に難しくなり、End to end testで全てテストしないといけなくなる。そうすると以下のようなピラミッドになる。 これでテストしていくとすごく時間がかかる。
https://youtu.be/VJi2vmaQe6w?t=463 より
image.png

レイヤーでモジュールを分けた場合

DIを導入し、依存を切ることができる。そして、ユニットテストをすることができる。
しかし、複雑にアプリケーションが成長した場合、レイヤーよりも機能単位で成長していることに気づく。また、この方法でモジュール化したにもかかわらず、モジュール下のレイヤーの変更はアプリすべての再ビルドを引き起こす。そして、レイヤーがそれ自体がモノリスになっているため、巨大なend-to-endが作られてしまう。ユニットテストが書けることは良いが、end-to-endテストとunit testが膨らんだテストになってしまう。これの問題はユニットテストのFidelityの低さを、低速で重いend-to-endのテストで補おうとしていることだ。 大きなend-to-endのテストへ過剰に頼ると長い間テストが必要になってしまう。そしてフォーカスできないためバグを見つけるのを難しくする。
効果的(effective)なモジュール化なしではすべての変更で、全てのコードが再ビルドされ、全てのテストが走ってしまう。
これらのキーとなるポイントはチームのベロシティを下げてしまう。しかしコードの構造をちゃんとすれば、大きなテスタビリティへの影響と開発スピードを持つことができる。

https://youtu.be/VJi2vmaQe6w?t=546 より
image.png

プロジェクトを分解(Decompose)しよう

サンプルのTODOアプリではタスクを管理する部分と
プログレスモジュールがあり、これはタスクに依存している。
そして、タスクはたくさんの機能からなる事がわかる。追加、詳細、リスト表示など。
そしてこのアプローチはスケールし、グロースさせられる。例えば新しい機能も追加でき、また機能が複雑になったときに細かくブレイクダウンできる。
コンポーネントを独立させられるため、もっとフォーカスしたテストを書くことができる。

https://youtu.be/VJi2vmaQe6w?t=641
image.png

End-to-end test

Critical User Journeyというものがある。
プロジェクトでCritical User Journey(重要なユーザーの工程)を定義してから初める。

Critical User Journey

  • Critical User Journeyはステップバイステップでユーザーがアプリケーションですることが定義されている。
  • 決められた最終的なゴールに向かいます。
  • そのJourney(工程)はおそらく複数の画面で、ゴールに向けて決定するポイントがいくつもある。
  • そしてそれらはときどきモックの一連としてSketchされる。

Critical User Journeyの例はこういう形

User journey1: Creating a new task. (タスクを作る)

まずホームのタスクのリストにたどり着き、それは空になっていて、
追加ボタンを押し、TODOの詳細を入れ、
そしてセーブボタンを押し、
ホーム画面に戻り、新しいタスクが見れている状態になる。

End to end testでこれをカバーしよう。

  • Androidの環境で動くかを確かめる
  • できるだけリリースのアプリの依存関係を使う
  • ユーザーが使うのと同じようにテストする。つまりブラックボックステストになる。

網羅的である必要はない。それは他のレイヤーのテストの仕事。

Integration test

協調して動作していることを確かめる。End-to-endに頼らなくてすむようにするために大切。ここではすべて本物のコンポーネントを使うことはそこまで大切ではない。

実際のオブジェクトを使うと、遅かったりする、ビルドに時間がかかったりする、不特定のネットワークに依存してテストが失敗しやすくなったり、コントロールできなかったりするので、test doubleを検討する

  • Dummy: 依存関係を満たすためだけに値を返すもの
  • Stub: テストに必要な特定の動きを設定できる。これのためにMockitoが使える。
  • Fake: 正確でリアルな動きをする軽量なクラス
  • Real object: テストが読みやすく、堅牢になる。例えばValue Objectとかは常に本物を使ったほうが良い。

タスクを追加するFragmentのテストの例

  • Activtyはnavigationについて関心があるが、このレベルではここのテストはせずFragmentで良い。
  • NavigationControllerをtest doubleに置き換えて呼び出しを確かめる
  • リモートのDataSourceは遅く失敗しやすいので、Test doubleで置き換える
  • EspressoのAPIでテキストを入力させたり、クリックさせる
  • 2つのことを確認する
    • タスクがちゃんと保存されたかを確認する
    • 正しい画面に遷移したか?

https://youtu.be/VJi2vmaQe6w?t=1453 より
image.png

次にRepositoryのテストの例

  • Interfaceに対してテストを書く
  • FakeRepositoryに対してもプロダクションコードと同じテストを回す

    • このFakeを使うとビルドの時間を短くできたりもっと軽量のテストを書ける https://youtu.be/VJi2vmaQe6w?t=1658 より image.png
    • Fakeが信頼が置けるものになる

 * このRepositoryに依存するFragmentのIntegration TestなどでこのFakeのリポジトリが使い回せる

https://youtu.be/VJi2vmaQe6w?t=1708 より
image.png

Integration Testで全ての入力バリデーションなどができるか?それはユニットテストでやる。

Unit test

  • 小さい単位のコードを保証する。
  • ローカルで走る。
  • 網羅的にテストされる。
  • とても速い。そしてたくさん作られる。

プロダクションのdependenciesを交換して良いが、テストはブラックボックスであるべき。実装ではなく、挙動をテストしたい。
テストの境界はぼやけることがある。

  • 通常LocalDataSourceをテストしようとして、DaoをMockitoでモックすると実装の詳細を知りすぎることになる。そのため、振る舞いが同じなのに、実装を変えるときにテストも変えないといけなくなる
  • これはchange detector testとして知られている。これはすぐにメンテナンスを難しくする。 効果的なユニットテストは実装の代わりに振る舞いにフォーカスする。どのようにそれをするか?

image.png

Repositoryと同じようにTaskDaoを使ったFakeを作るか?おそらくTaskDaoはモジュールのpublicなAPIにならないので、このFakeを共有できず、それ以外の利益が無さそうなので作らない。
そのため、実際のRoomのDAOを使おう。Roomを使うとメモリ内にDBが作れる。

そしてこれはまだユニットテストか?結合テストになったか?多くの人が反対し、その境界がぼやけるかもしれない。
しかし、重要なポイントはテストでは実際の依存関係を使用することを恐れないこと。それによって読みやすく軽量で堅牢なテストが書ける。

まとめ

End-to-end testではCritical User Journeyをカバーしよう。
機能を分解し、integration testをしよう。テストするときにはUIからデータレイヤーに縦にスライスして (画面でスライス)してテストしよう。
次にモデル(Repository)に対してテストを書こう。これに他のモジュールが基づいているのでキーとなる。そしてUIやdatastoreなどに対する小さい単位のintegrationとunit testになる。
明確にモジュール間の契約を定義してコードベースを小さくすることで、ビルドを合理化し、ビルド時間を短縮し、交換できる検証されたFakeをExportできる。これによりテストを重たい本番環境への依存から切り離す。
アプリに自信を持たせるためにEnd-to-endテストを行うことはできるが、大部分をここにもってくるべきではない。モジュール化し、小さくすることでより焦点を絞ったテストにできる。そしてモジュールは切り離されている状態にする。
テストする場所を決める必要がある。どこをテストするのかドキュメント化して他のメンバーと共有する事が大事。



動画を見た雑なメモ書き


建造物でも、のこぎりがでてきたり、ツールやパターンや方法が進化してきた。ソフトウェアでも一緒。
Android Development ToolでもEclipse、Android Studiooになったり、Jetpackになったり。。
ただ、こうした進化は常に簡単ではなく、どのように設計するか、どのようにコードベースを管理するのか、どのようなライブラリを選ぶのか、どのようなツールを選ぶのか。この選択は開発の早い段階に行われる。これはアプリのテスタビリティに長い影響を与える。そして、それにより、開発速度、継続的に新しい機能の追加に影響を与える。
実際の例によって、どのように長期のテストの戦略を作るかを説明します。

テストにおいて、キーとなるアトリビュートはスコープです。
スコープは一つのメソッドでテストがアプリケーションのどのぐらいカバーしているのか、または複数の機能、複数の画面にまたがることはできる。スコープはテストの他の2つのアトリビュートに影響を与える。スピード、ミリ秒単位で終わるものもあれば何分もかかるものもある。Fidelity(忠実さ)、テストケースのシミュレートが、どのぐらい本当の世界の(シナリオに近いか。スコープを広げれば広げるほど、あなたのテストのFidelityは上がる。しかし、逆にスピードを遅くする。一つのテストではすべてにおいて一番いいものはできない。

いつ十分いいという状態になるの?いつパーフェクトになるの?いいバランスにするにはどうすればいいの?テストピラミッドはバランスを作るガイドとして利用される。ピラミッドの上の方ではFidelity(忠実さ)が高く、スコープが広がる、しかし、スピードやフォーカスやスケーラビリティがないというピラミッドです。

ユニットテストは速く、軽く、フォーカスされていて、スケールしやすい。一つのメソッドについてテストするだけなので、簡単に定義できる。これは失敗した場所にフォーカスできる。

インテグレーションテストではいくつかのユニットを一緒にするものです。それらのコラボレーションに関心がある。一緒にして、全体で期待した動きになっているかどうかを見る。

End-to-endテストはアプリケーションのキーパスをカバーする。複数の画面や複数の機能。アプリをどうテストすればよいのか知っているので、簡単に定義できる。

新しいTODOアプリケーションを作りました。Google Officialではないが、本当のアプリになっている。Android testing codelabの一部になっている。
https://github.com/android/testing-samples

これまで対面した議論などを紹介していきます。
アプリケーションを作るときにいくつかのキーとなるCritical User Journey(重要なユーザーの工程)を定義してから初めます。
Critical User Journeyはステップバイステップでユーザーがアプリケーションですることです。
また、決められた最終的なゴールに向かいます。
そのJourney(工程)はおそらく複数の画面で、ゴールに向けて決定するポイントがいくつもあります。
そしてそれらはときどきモックの一連としてSketchされる。

Critical User Journeyから実装を始めよう。
そしてTODOアプリでUXデザイナが送ってきたものの最初が以下になる。

User journey1: Creating a new task. (タスクを作る)

まずホームのタスクのリストにたどり着き、それは空になっていて、
追加ボタンを押し、T
ODOの詳細を入れ、
そしてセーブボタンを押し、
ホーム画面に戻り、新しいタスクが見れている状態になる。

User journey2: Checking your progress (タスクの進捗を見る)
ユーザーは存在するタスクを選択でき、
そしてタスクの完了をマークでき、
統計ページに行き
そしてその進捗を見ることができる。

すべてのプロジェクトは小さく始まる。もしデザインや設計、構造に意識を払わない場合、開発は早い段階で制御不能になる。何も考えずに作るとでかいモノリス、スパゲッティのようなボールになり、一貫しない依存関係について考えるのも難しいが、テストも困難にする。
もし個々の単位が高凝集で低結合などのキーとなる原則に従わない場合、独立によってテストすることが本当に難しくなる。それだけでなく、このようなモノリスなコードベースでは一つの変更を加えるとすべてを再ビルドする必要がある。そしてこの事実は大きいend to endを作らないといけなくなる。テストピラミッドが逆になってしまい、テストが不均一になってしまっている。
もし構造を作ることを考えて、レイヤーアーキテクチャーを考えたとしましょう。 開発の最初の段階ではこれが分けられる最初の部分で、各レイヤーにマッピングできるものがあるので、良さそうです。
このようにコードを構成した場合、高凝集で低結合といった原則を取り入れ、そしてDIを導入し、依存を切ることができる。そして、ユニットテストをすることができる。しかし、複雑にアプリケーションが成長した場合、レイヤーよりも機能単位で成長していることに気づく。また、この方法でモジュール化したにもかかわらず、モジュール下のレイヤーの変更はアプリすべての再ビルドを引き起こす。そして、レイヤーがそれ自体がモノリスになっているため、巨大なend-to-endが作られてしまう。ユニットテストが書けることは良いが、end-to-endテストとunit testが膨らんだテストになってしまう。これの問題はユニットテストのFidelityの低さを低速で重いend-to-endのテストで補おうとしていることだ。
バランスが取れたピラミッドを作るためのガイドがない。

そのため、あまり構造化、設計されていないコードベースは開発ワークフローでボトルネックを引き起こしやすい。大きなend-to-endのテストへ過剰に頼ると長い間テストが必要になってしまう。そしてフォーカスできないためバグを見つけるのを難しくする。
効果的(effective)なモジュール化なしではすべての変更で、全てのコードが再ビルドされ、全てのテストが走ってしまう。
これのキーとなるポイントはチームのベロシティを下げてしまう。しかしコードの構造をちゃんとすれば、大きなテスタビリティへの影響と開発スピードを持つことができる。

プロジェクトを解体することを考えてみよう。
TODOアプリではタスクを管理する部分と
プログレスモジュールがあり、これはタスクに依存している。
そして、タスクはたくさんの機能からなる事がわかる。追加、詳細、リスト表示など。

そしてこのアプローチはスケールし、グロースさせられる。新しい機能も追加でき、また機能が複雑になったときに細かくブレイクダウンできる。
このアプローチはただActivityであるというだけでまとめられるというアプローチよりも、同じドメインのコンポーネントのほうが機能的に関連しているため。
これはGradleのモジュールやBazelのライブラリで実現できる。

ドメイン指向のモジュールをアプリに追加でき、それらのAPIの境界でインタラクションを取り決められる。
そして、コンポーネントを独立させられるため、もっとフォーカスしたテストを書くことができる。
最後にintegration testの設計を見ましょう
もちろん全てのモジュールが解体されているので、ユニットテストも可能になっている。そしてユニットテストも可能になっている。そして大きいEend-to-end testも書くことができる。さらに、この構成によって新しい機能がスケールでき、それとともにテストもスケール可能になる。

このガイドをスタートポイントとして使うことができる。そして、もちろん、違った形でアプリを解体することができる。

アプリケーションを作るときにdata bindingやview model、live data, navigation, roomといったJetpackからアーキテクチャコンポーネントを利用した。そしてアプリの設計をMVVMに則った。これはとても良く関心事を分けられる。そして、Jetpackライブラリはこれをうまく行うことができる。
Single Activityから初め、navigation componentを使って、ユーザーのフローに対応付け、Fragmentを管理する。
それぞれのFragmentはそれぞれのxmlを持っており、それぞれのViewModelを持っている。。。。(アーキテクチャの紹介) モデルレイヤーはRepositoryに抽象化されている。

TDDのアプローチであれば、最初はEnd-to-endテストから始める。

End to endテスト

  • Androidの環境で動くかを確かめる
  • できるだけリリースのアプリの依存関係を使う
  • ユーザーが使うのと同じようにテストする。つまりブラックボックステストになる。

網羅的である必要はない他のレイヤーのテストの仕事になる。

Integrationテスト

強調して動作していることを確かめる。End-to-endに頼らなくてすむようにするために大切。ここではすべて本物のコンポーネントを使うことはそこまで大切ではない。
スコープを狭める候補何があるか?
フラグメント単位。
test doubleを使う。(遅かったりする、ビルドに時間がかかったりする、不特定のネットワークに依存してテストが失敗しやすくなったり、コントロールできなかったり)

  • Dummy: 依存関係を満たすためだけに値を返すもの
  • Stub: テストに必要な特定の動きを設定できる。これのためにMockitoが使える。
  • Fake: もっと正確でリアルな動きをする。
  • Real object: テストが読みやすく、堅牢になる。例えばValue Objectとかは常に本物を使ったほうが良い。

AddEditTasksFragmentTest

  • Activtyはnavigationについて関心があるが、このレベルではここのテストはせずFragmentで良い。
  • NavigationControllerをtest doubleに置き換えて呼び出しを確かめる
  • リモートのDataSourceは遅く失敗しやすいので、Test doubleで置き換える
  • EspressoのAPIでテキストを入力させたり、クリックさせる
  • 2つのことを確認する
    • タスクがちゃんと保存されたかを確認する
    • 正しい画面に遷移したか?

TaskRepositoryTest

  • Interfaceに対してテストを書く
  • FakeRepositoryに対してもプロダクションコードと同じテストを回す
    • このFakeを使うとビルドの時間を短くできたりもっと軽量のテストを書ける
    • Fakeが信頼が置けるものになる → * このRepositoryに依存するすべてのテストAddEditTasksFragmentTestなどでこのFakeのリポジトリが使える

ユニットテスト

全ての入力のバリデーションのケースで、これが保証できるか?
ユニットテストはここで使われる。

小さい単位のコードを保証する。
ローカルで走る。
網羅的にテストされる。
とても速い。そしてたくさん作られる。
プロダクションのdependenciesを交換して良いが、テストはブラックボックスであるべき。実装ではなく、挙動をテストしたい。
ここでのテストの境界はぼやけることがある。
- 通常LocalDataSourceをテストしようとして、DaoをMockitoでモックすると実装の詳細を知りすぎることになる。そのため、振る舞いが同じなのに、実装を変えるときにテストも変えないといけなくなる

これはchange detector testとして知られている。これはすぐにメンテナンスを難しくする。
効果的なユニットテストは実装の代わりに振る舞いにフォーカスする。どのようにそれをするか?

まずは実装方法として非同期になるので、確実に終わるようにrunBlocking{}などを使おう。
呼び出しをmockする方法だとすぐに汚くなってしまう。そのためRepositoryと同じようにTaskDaoを使ったFakeを作るか?おそらくTaskDaoはモジュールのpublicなAPIにならないので、このFakeを共有できず、それ以外の利益が無さそうなので作らない。そのため、実際のRoomのDAOを使おう。Roomを使うとメモリ内にDBが作れる。
そしてこれはまだユニットテストか?結合テストになったか?多くの人が反対し、その境界がぼやけるかもしれない。しかし、重要なポイントはテストでは実際の依存関係を使用することを恐れないこと。それによって読みやすく軽量で堅牢なテストが書ける。

まとめ

end-to-end testではcritical key user journeyをカバーしよう。
機能を分解し、integration testをしよう。テストするときにはUIからデータレイヤーに縦にスライスして (画面でスライス)してテストしよう。
次にモデル(Repository)に対してテストを書こう。これに他のモジュールが基づいているのでキーとなる。そしてUIやdatastoreなどに対する小さい単位のintegrationとunit testになる。
明確にモジュール間の契約を定義してコードベースを小さくすることで、ビルドを合理化し、ビルド時間を短縮し、交換できる検証されたFakeをExportできる。これによりテストを重たい本番環境への依存から切り離す。
アプリに自信を持たせるためにEnd-to-endテストを行うことはできるが、大部分をここにもってくるべきではない。モジュール化し、小さくすることでより焦点を絞ったテストにできる。そしてモジュールは切り離されている状態にする。
この方法で作ったアプリでは自然にいくつかの切断できるポイントができる。独自のテストする場所を決める必要がある。あるプロジェクトで動作しても他のプロジェクトでは動作しない場合があるので、どこをテストするのかドキュメント化して他のメンバーと共有する事が大事。

今後のAndroidではユニットテストでも端末のテストでも両方で使えるAPIを作っていて、それがどのようになるかやっていっている。


22
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
22
11