[Android]MockとFakeどちらの方がいいのか?
Androidで、Unitテストを実装する際にMockライブラリ(Mockitoやmockk)などを使うのか、それともFakeのクラスを新しく作ってそのクラスを使用してUnitテストを実装するのか、どちらがいいのかいまいちわからなかったので、今回調べてみました。
結論から言うと自分は、MockよりFakeの方が多くのケースではいい選択肢となるのではないかと思いました。
テストを実装するときに考慮すべきポイント
テストを実装する時に、考慮すべきポイントが3つ公式で紹介されています。
- Scope
- アプリのどの範囲までカバーするのか?
- テストを実装する際に、アプリの全体をカバーするのか?それとも範囲の小さい部分をカバーするのかを決める必要があります。
- Speed
- テストの実行スピードはどのくらいか?
- 当然テストの実行スピードは速いほうがいいので、ここも意識する必要があります。
- Fidelity
- テストがどの程度実際の実装に近いものか?
- テストコードが実際のプロダクションコードに近ければ近いほどよりリアルで意味のあるテストを作成することができます。
テストを実装する際に、できるだけ実際のケースに忠実にテストした方がいいと思うので、ベストなのは、実際に使用しているクラスを使用すること。ただそれだと色々問題が出てくるケースもあると思うので、その時にFakeやMockを使用する形になると思います。
テストを実装するときは、できるだけユーザー目線でなるべく本番に近い形でできるように工夫していくことが大切です。
今回のFakeとMockどっちの方がいいのかを判断する時に、一番重視すべきポイントは、3のFidelity(忠実度)にあると思うので、そこについてみていきます。
FakeとMockのそれぞれのメリット・デメリット
では、それぞれのメリット・デメリットを挙げてみます。
Fake
メリット
- 実際のコードに近い環境でテストを実行できるので、より正確なテストの結果を得ることができる。
- Fakeの対象のクラスの内部実装まで正確に把握する必要がない。(対象のクラスに変更があった場合に、柔軟に対応できる)
デメリット
- 新しくクラスを作成する必要があるので、コードの記述量が多くなる。
Mock
メリット
- Mockしたクラスのメソッドの挙動を指定していくので、Mock対象のクラスの内部まで理解していれば、書きやすく見やすい。
デメリット
- Mockライブラリの使い方について学ぶ必要がある。(Mockitoやmockkなど)
- Mockしたクラスの変更に対応するのが大変
なぜMockよりFakeの方がいいのか?
上記のメリット・デメリットを含めてなぜ自分がFakeの方がいいと感じたかというと、一番の理由は変更に対してFakeの方が柔軟に対応できると思ったからです。
Mockの場合、Mockしたクラスの内部実装まで把握していないと、ちゃんとテストを書くことができないと思います。
一度書いたコードが今後一切変更されることがないならばいいですが、大半の場合書いたコードはアップデートされ続けていきます。
もし、Mockを使用してテストを実装した場合、その変更に対して開発者自身が変更を把握していないと意図しない挙動になる可能性があります。
Mockの対象が変更されることによって、動作を指示するためのコードの量が増え、テストが見にくくなる問題も出てきます。
また、新しくチームに入ってきたメンバーは実装に慣れていないため、理解しにくいとも思います。
Androidの公式ドキュメントでもMockよりもFakeを推奨するとの記述があります。
モックよりもフェイクを優先します
参考
https://developer.android.com/topic/architecture/recommendations#testing
これらの理由からFakeの方がテストを実装する際にいい選択肢になるのではないかと思いました。
Fakeでテストを書いてみる
実際にFakeでテストを書いてみます。
今回は簡単な、Repositoryのテストをやってみたいと思います。
まず、FakeUseRepository
を定義します。
本来のUseRepository
では、データソースからユーザーの情報を取得してその値を返しますが、Fakeなのでクラスの中で値を持ってその値を返すようにします。
もし、nameがnullだった場合何らかの例外を投げるようにしています。
そして、Fakeのテストを実装するにあたって、実際のコードの挙動に近づけるためにテスト専用のメソッドを用意して、そのメソッドを通してFakeクラス内にある値を変更するようにします。
class FakeUserRepository : UserRepository {
private var userInfo: User? = User(
name = "demo",
age = 21,
)
override suspend fun fetchUserInfo(): User? {
return if (userInfo == null) {
throw Throwable("error")
} else {
userInfo
}
}
// test only
fun setInvalidUserInfo(invalidUserInfo: User?) {
userInfo = invalidUserInfo
}
}
実際にテストするUseCaseの実装を見てましょう
class UserUseCaseTest {
private lateinit var fakeUserRepository: UserRepository
private lateinit var userUseCase: UserUseCase
@Before
fun setUp() {
fakeUserRepository = FakeUserRepository()
userUseCase = UserUseCase(fakeUserRepository)
}
// ユーザー情報の取得が成功した場合
@Test
fun fetchUserInfo_Success() {
val expected = User.fake()
val actual = userUseCase.fetchUserInfo()
Assert.assertEqual(expected, actual)
}
// ユーザー情報の取得に失敗した場合
@Test
fun fetchUserInfo_Failure() {
// ユーザー情報にnullを設定
fakeUserRepository.setInvalidUserInfo(null)
val actual = userUseCase.fetchUserInfo()
assertThat(actual).isEqualTo(Throwable("error"))
}
}
fun User.Companion.fake(
name: String = "some name",
age: Int = 20
): User {
return User(
name = name,
age = age,
)
}
fake
メソッドを定義することで、仮にサーバーからの返り値が一つ増えたとしても、いちいち全てのテストを書き換える必要がなくなるので、スケールに対応することができます。
何かテストをするときに異常系を設定しようとする際は、Fakeで作成したテスト専用のメソッドを使って異常値を設定し、その結果実際にテストするUseCaseが意図した挙動をするか確かめるようにすれば、実際のコードに近い形でテストを作成することができます。
終わりに
今回は、FakeとMockどちらがいいのかを見ていきました。
多くのケースではFakeの方がいいのではないかと自分は思いましたが、Mockの方がいいケースも存在すると思うので、ケースバイケースで対応していく必要がありますが、基本的にはFakeの方がいいのではないかと思いました🙆♂️
参考
Discussion