Dagger HiltというGoogleがAndroid開発において推奨するDIライブラリ(JVM言語であれば使える)のこちらのドキュメントについてです。
https://dagger.dev/hilt/testing-philosophy
結構良いテストについて個人的に刺さる概念があったので、書いておきます。
またシンプルな部分以外のHiltの良いところが分かる気がします。
Dagger Hiltを踏まえたテストのプラクティスについて書かれている。
Dagger HiltのAPIや機能は何が良いテストを作るかの暗黙の哲学に基づいて作られている。ただ、良いテストというのが万人に受け入れられているわけではいので、Hiltチームにおいてのテストの哲学を明らかにするためのドキュメントとなる。
何をテストするのか
Hiltは外部の"ユーザーからの観点でテストする"ことを推奨する。外部のユーザーとはたくさんの意味がある。本当のユーザーも指すし、クラスやAPIの利用者も指す。
大切なところは"実装の詳細を表現してはいけない"ということ。内部の実装に依存したテスト、例えば内部のメソッドに依存したテストを書くとテストが壊れやすくなる。internalなメソッドの名前が変わっても、良いテストは何も変更する必要がない。今のテストを壊す要因はユーザーに見える変更があったときのみになる。
実際の依存関係を利用する
**Hiltのテストの哲学はすべてのクラスがそれぞれテストを書くことを強制しない。実際にはそのようなルールは"ユーザーからの観点でテストする"という原則に違反する。**テストは書きやすく、実行しやすくするために、必要なだけ小さくする。(高速、リソースを大量に消費しないなど)
他に違いがないのであれば、テストは以下のことを、この順番で優先する
- 依存関係の実際のコードを利用する
- ライブラリが提供する標準のFakeを利用する
- 最後の手段としてMockを利用する
しかし、これにはトレードオフがある。
- テストに置いての本物の依存関係をインスタンス化するセットアップはボイラープレートになりやすく、繰り返しになりやすい。
- バックエンドのサーバーを立ち上げる必要があるなど、パフォーマンスのトレードオフが存在する。
Hiltは最初の問題を解決する。(詳細は以下)パフォーマンスについては問題になることはあるが、ほとんどの場合では問題にならない。I/Oで依存関係がある場合のみ問題になる可能性がある。パフォーマンスを大幅に低下させることなく、実際の依存関係を利用して便利かつ堅牢に利用できる場合は、実際の依存関係を利用する必要がある。これによってテストで大きな悪い影響がある場合は、Hiltはそのバインディングを交換する方法を提供する。
より多くの実際の依存関係を使うことの大きなアドバンテージ
- 実際の依存関係は本当の問題を補足しやすい。モックのように古いまま残されたりしない。
- "ユーザーからの観点でテスト"と組み合わせることで、同じカバレッジでもっと少ないテストの量で書くことができる。
- テストが壊れることが、FakeやMockの設定ミスによる問題による問題の代わりに、実際の問題を指し示す (そして、逆に言えばテストがパスすることはコードがちゃんと動くことを意味する)
- "ユーザーからの観点でテスト"と"実際の依存関係を使う"は相性が良い。依存関係を入れ替えないため。
もし本当の依存関係が使えないのであれば、次にライブラリが提供する標準のFakeを使う。ライブラリの作者や堅牢なカバレッジで提供されることによってメンテされているのであれば、標準のFakeはMockより良い。これらの理由によりMockは最終手段となる。
HiltとDI、そしてTest
これらの基礎によって、"HiltとDI、そしてTest"に入る。"実際の依存関係を使う"についてのDagger Hiltの答えはTestでDI/Daggerを使うことである。これはもっと実際に近い、なぜならプロダクションで行われるようにオブジェクトが作られるため。これはテストがプロダクションコードより壊れにくいことを意味し、実際のオブジェクトを使いやすくする。実際、@Inject
のコンストラクタがある場合は、Mockを作って入れるより、Daggerを使うほうが簡単で少ないコードにできる。
残念なことにHiltを利用しないでこのようなテストを行うことは、ボイラープレートとDaggerの設定の作業により、これまでは困難だった。しかしHiltはボイラープレートコードを生成し、FakeやMockが必要なときにテストに違った設定をセットアップできる。Hiltを使えばこの問題はDaggerでテストを作成することの妨げにならず、実際の依存関係を簡単に利用できる。
実際にどうやってHiltがTestで依存関係を置き換えるかはこちらに書いてあります。 https://qiita.com/takahirom/items/3231edf2a430569b3e9d#testing
他の解決策の欠点
ユニットテストでDaggerを使わない方法はとてもよくある方法である。これは残念なことに大きな欠点があるが、HiltなしでDaggerを利用することの難しさを考えると理解できる。
例えば、Fooクラスをテストしようとする。
class Foo @Inject constructor(bar: Bar) {
}
このケースでDaggerを使わない場合は単にコンストラクタを呼び出すだけ。一見これはとてもシンプルで理解できる、しかしFooのコンストラクタにBarを適応し始めると崩壊し始める。
テストでの直接のインスタンス化はMockを使うことを促進する
以前話した"実際の依存関係を使う"によって、本物のBarクラスをできるだけ使うべき。しかし、どのようにすれば良いだろうか?テストでFooクラスをテストで使うには、これは実際は再起になる: 自分でインスタンス化する必要があるので、Barもインスタンス化する必要があり、Barに依存関係を持っている場合、同様にそれらをインスタンス化する必要がある。深くなりすぎないようにするために、テストのスピードやパフォーマンスのためではなく、たくさんの壊れやすいボイラープレートコードはメンテナンスの問題を起こすため、FakeやMockを使い始める必要がある。これはFakeやMockを使い始めるには良い理由ではない。そして今これをすることを余儀なくされている。
以前に議論したように標準のFakeを利用する方法では、直接のインスタンス化をするメンテナンスの負荷を減らすことができる。しかし、必ずしもシンプルではない。(省略: 同様にFakeBarがClockに依存して、、などFakeも依存関係を管理しなくてはいけなくなる。)
これは通常、開発者をMockを使うことを促進する。**Mockは依存関係のチェーンによる問題を解決するが、静かに古いままのこされ残されたり、実際のバグを見つけるという全体的な目的でテストを役立たなくするという重大な欠点がある。**テスト作成者以外は誰もMockの動作チェックをしないため、時間が経過するとテストが有用なシナリオをテストしなくなる可能性がかなりある。
テストでの直接のインスタンス化は実装の詳細を表現する
直接のインスタンス化は"実装の詳細を表現してはいけない"というプラクティスを破る。なぜなら、その依存関係の詳細のコンストラクタを呼ぶため。Barが@Inject construcotr
がついている場合、Fooが実装の詳細をリファクタしたりする場合があるので、Barに依存することを知る必要はない。
この点について説明すると、Foo(Bar, Baz)のようにFooがBarやBazといった依存関係を持っていたときに、Daggerではこのパラメーターの順番を変えても何も起こらない。しかし、直接インスタンス化していた場合はテストを変更する必要がある。同様に新しい@Injectクラス
やオプショナルバインディングを追加する場合もプロダクションでは変更する必要がないが、テストでは変更が必要になる。
まとめ
Hiltは実際の依存関係を使って簡単にテストを書くために、DaggerをテストでHiltを使うデメリットを治すように設計されている。Hiltを使ったテストはこれらの哲学に従う場合、全般的に良い体験になる。