Test Doublesとは、実際の依存関係の代わりに単体テストで使用される代替オブジェクトです。この用語は、Gerard Meszarosが著書「xUnit Test Patterns」(2007年)でMock、Stub、Fake、Spy、Dummyの総称として導入しました。Martin Fowler(2024年)によると、Test Doublesはテスト対象のコンポーネントをその環境から分離し、テストを決定論的で高速かつ外部サービスに依存しないものにします。
重要ポイント
Test Doublesは自動車業界(スタントダブル)からソフトウェア開発に取り入れられた用語です。スタントダブルが危険なシーンで俳優の代わりをするように、Test Doubleはテストシナリオで実際のコンポーネントを置き換えます。これは、実際の依存関係が利用不可、低速、非決定的、または副作用がある場合に必要です。
Test Doubleの概念は5つの特定のタイプを包含し、それぞれが独自の役割を解決します。Meszarosの分類は標準的であり、すべての現代的なテストガイドで使用されています。タイプ間の違いは、制御と検証の度合いにあります:単純なパラメータ埋め(Dummy)から完全な呼び出しシーケンス検証(Mock)まで。
Test Doublesの主な目的は、テスト対象モジュールの分離です。モバイル開発では、実際の依存関係にはAPIサーバー、データベース、ファイルシステム、デバイスセンサー、システムサービス(LocationManager、Camera、Bluetooth)が含まれます。これらのコンポーネントを直接使用すると、テストが低速で壊れやすく、環境に依存するものになります。Google Testing Blog(2023年)によると、適切に分離された単体テストはミリ秒で実行され、統合テストは秒や分で実行されます。
Gerard Meszarosの分類には5つのタイプのTest Doublesが含まれ、動作と目的が異なります。それらの違いを理解することが、適切な単体テストの基礎です。
Dummyは、テスト対象メソッドに渡されるが決して使用されないオブジェクトです。Dummyはメソッドシグネチャを満たすためだけに必要です。Kotlinでは、これは多くの場合null、emptyList()、またはスタブ付きのオブジェクトです。Dummyにはロジックを含めるべきではありません — 呼び出された場合、テストは失敗する必要があります。
Fakeはインターフェースの簡略化されたしかし動作する実装です。MockやStubとは異なり、Fakeには実際のビジネスロジックが含まれますが、簡略化された形です。典型的な例はInMemoryUserRepositoryで、データベースの代わりにHashMapにデータを保存します。Fakeは、実際のインフラストラクチャのオーバーヘッドなしに、状態に依存するロジックをテストする必要がある場合に使用されます。
| タイプ | 目的 | 例 |
|---|---|---|
| Dummy | パラメータを埋める | null、空オブジェクト |
| Fake | 動作する簡略化された実装 | InMemoryRepository |
| Stub | 固定値を返す | when(api.getUser()).thenReturn(user) |
| Spy | 検証のために呼び出しを記録 | verify(spy).save(user) |
| Mock | 相互作用を検証 | verify(mock).sendEmail(email) |
Stubは特定の呼び出しに対して事前定義された値を返します。Stubは呼び出されたかどうかを確認しません — 単にデータを提供します。Mockitoでは、Stubはwhen(method).thenReturn(value)で作成されます。Stubは、依存関係が特定の値を返す必要があるが、呼び出し自体は重要でない場合のテストに理想的です。
Spyは実際のオブジェクトのラッパーで、後で検証するためにすべての呼び出しを記録します。Mockとは異なり、Spyは実際のオブジェクトに呼び出しを委譲しますが、それらが発生したことを確認できます。Mockitoでは、Spyはspy(realObject)で作成されます。Spyは部分的なモッキングに便利で、実際のオブジェクトを使用したいが一部の呼び出しを検証したい場合に使用します。
Mockは事前定義された呼び出し期待値を持つオブジェクトです。Mockは、特定のメソッドが特定の引数と特定の順序で呼び出されたことを検証します。Stubとは異なり、Mockはデータの返却ではなく動作の検証に焦点を当てています。Mockはモバイル開発で最も強力で最も頻繁に使用されるTest Doubleのタイプです。
MockとStubの違いは、経験豊富な開発者の間でも混乱を引き起こすことがよくあります。主な違いは目的にあります:Stubは状態検証(state verification)を行い、Mockは動作検証(behavior verification)を行います。
Stubは次の質問に答えます:「コードは正しい結果を返したか?」。Mockは次の質問に答えます:「コードは正しい引数で正しいメソッドを呼び出したか?」。モバイル開発では、結果が重要な場合(例:リポジトリからのデータ)にStubが使用され、副作用が重要な場合(例:メール送信、データベースへの書き込み)にMockが使用されます。
// Stub: 状態検証
every { repository.getUsers() } returns listOf(user)
val result = useCase.getUsers()
assertEquals(1, result.size)
// Mock: 動作検証
every { analytics.logEvent("purchase") } returns Unit
useCase.purchase(item)
verify { analytics.logEvent("purchase") }
MockKを使用したKotlinでの5つのタイプすべてのTest Doublesの実践的な例 — Androidプロジェクトで最も人気のあるモッキングライブラリです。
class InMemoryUserRepository : UserRepository {
private val store = mutableMapOf<String, User>()
override fun save(user: User) {
store[user.email] = user
}
override fun findByEmail(email: String): User? {
return store[email]
}
}
class RegisterUseCaseTest {
private val api = mockk<AuthApi>()
private val repo = spyk(InMemoryUserRepository())
private val useCase = RegisterUseCase(api, repo)
fun `register user successfully`() = runTest {
// Stub: 固定APIレスポンスを返す
coEvery { api.register("test@test.com") } returns AuthResult.Success("token123")
val result = useCase.execute("test@test.com")
// Verify: ユーザーが保存されたことを確認
verify { repo.save(any()) }
assertTrue(result.isSuccess())
}
}
data class Logger(val appContext: Context, val format: FormatType)
fun `test logger with dummy context`() {
// Dummy: ContextはLogger内で使用されない
val dummyContext = mockk<Context>()
val logger = Logger(dummyContext, FormatType.JSON)
assertEquals(FormatType.JSON, logger.format)
}
Test Doubleのタイプ選択は、何をテストしているかに依存します:状態、動作、または統合。AndroidおよびiOSのモバイル開発では、以下の推奨事項が確立されています。
ViewModelをテストする際は、副作用を生成する依存関係(リポジトリ、アナリティクス、ナビゲーション)にはMockを使用し、データを返す依存関係(APIクライアント、ContentProvider)にはStubを使用します。これにより、ViewModelが成功シナリオとエラーシナリオの両方を正しく処理することを検証できます。
Repositoryレベルでは、Fake(インメモリデータベース実装)とStub(固定APIレスポンス)を優先します。Fakeを使用すると、SQLiteを設定せずにキャッシュロジックとオフラインモードをテストできます。StubはさまざまなHTTPステータス(200、404、500、timeout)をシミュレートします。
Test Doublesの誤った使用は、リファクタリングのたびに壊れる脆弱なテストの最も一般的な原因の一つです。
最も一般的な間違いは、すべてをモックすることです。テスト内のすべての依存関係がMockに置き換えられると、テストは実際の動作を検証しなくなります。Mockは外部依存関係に対してのみ使用すべきです(ネットワーク、データベース、ファイルシステム、システムサービス)。内部アプリケーションコンポーネント(Value Object、data class、単純なユーティリティ)は置き換えるべきではありません。
2つ目の間違いは、期待値を定義せずにMockを作成することです。every / whenなしでメソッドが呼び出されると、Mockはデフォルト値(null、0、false)を返します。これにより、Mockが静かにnullを返し、テストがそれを正しい動作として解釈する偽陽性テストにつながる可能性があります。
3つ目の間違いは、すべてのMockのすべての呼び出しを検証することです。Verifyは、ビジネスロジックの観点から重要で重要な呼び出しにのみ使用する必要があります。過剰な検証はテストを脆弱にします:プロダクションコードでの呼び出し順序の変更が、動作を変更せずにテストを壊します。
よくある質問
Stubはデータを返し、状態(何が返されたか)を検証します。一方Mockは動作(どのメソッドが呼び出されたか)を検証します。Stub = 「Xを返せ」、Mock = 「Yが引数Zで呼び出されたことを確認せよ」。実際のテストでは、1つのオブジェクトがStubとMockの両方として同時に機能することがよくあります。
Fakeは、状態に依存するロジック(キャッシング、オフラインモード、トランザクション)をテストする場合にMockより推奨されます。Fake(インメモリ実装)は、脆弱なverify呼び出しなしでこれらのシナリオをテストできます。Mockはデータ送信(アナリティクス、プッシュ、メール)の確認に適しています。
KotlinのAndroidプロジェクトにはMockKをお勧めします。追加設定なしでコルーチン、サスペンド関数、シールドクラス、拡張関数をサポートしています。Javaプロジェクトでは、Mockitoが引き続き標準です — 広範なドキュメントを持つ最も人気のあるライブラリです。
Kotlin Flowをテストするには、MockKと一緒にTurbineライブラリを使用します。TurbineはFlowの出力検証を簡素化します:値の順序、ストリームの完了、例外を確認できます。Flow用のStubはflowOf(value)を返し、MockはFlowが収集されたことを検証します。
はい、ただしAPIレスポンスのレベルであり、UIコンポーネントではありません。MockWebServer(OkHttp)やWireMockなどのライブラリを使用すると、UIテストでHTTPレスポンスをモックできます。UIコンポーネント自体(Compose、SwiftUI Views)は置き換えるべきではありません — それらの動作はスクリーンショットテストとEspressoを通じてテストされます。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。