Fakeとは、実際のコンポーネントのように振る舞うが、本番インフラストラクチャの代わりにインメモリストレージやその他の軽量メカニズムを使用する、依存関係の簡素化された動作可能な実装です。Stubとは異なり、fakeには実際のビジネスロジック(ソート、フィルタリング、集約)が含まれていますが、外部効果はありません。Roomの代わりにインメモリデータベース、SharedPreferencesの代わりにHashMapが典型的な例です。詳細はMartin Fowlerのテストダブル分類をご覧ください。
主要ポイント
Fakeとは、テストに適したインターフェースの完全ながら軽量な実装です。この用語はGerard Meszaros(2007)が著書“xUnit Test Patterns”で紹介しました。ハードコードされた回答を返すstubとは異なり、fakeには実行可能なコードが含まれています。リストのソート、条件によるフィルタリング、レコードのカウントが可能です。本番実装との唯一の違いは、fakeがインメモリデータで動作し、実際のI/O操作を実行しないことです。
主な利点は速度です。Fakeを使用したテストは、ディスク、ネットワーク、データベースへのアクセスがないため、ミリ秒単位で実行されます。インメモリのHashMapはRoomやCoreDataよりも100〜1000倍高速に動作します。同時に、fakeは実際のビジネスロジック(ソート、フィルタリング、集約)をテストします。これらはstubがテストできないものです。なぜならstubは指示されたものだけを返すからです。Fakeは、コードが所定の回答を受け取るだけでなく、データを正しく処理するという確信を与えます。
FakeがStubより望ましい — テスト対象のコンポーネントがデータに対して複数の操作(取得、フィルタリング、ソート、保存)を実行する場合、stubでは各呼び出しを個別に設定する必要があります。Fakeは内部にロジックを含んでいるため、テストは単にメソッドを呼び出して結果を確認するだけです。IT Sectrでは、単体テストですべてのリポジトリにfakesを使用しています。HashMapを使用したfakeリポジトリは、MockitoやMockKを設定せずに90%のシナリオをカバーします。
選択基準 — テストが何を検証するかを決定します:状態か相互作用か。テストが状態(作業の結果)を検証しロジックを使用する場合はfakeを使用します。テストにロジックなしの入力データのみが必要な場合はstubで十分です。テストがメソッドが呼び出されたことを検証する場合はmockを使用します。1つのテストでテストダブルの種類を混在させると、理解が複雑になり脆弱性が高まります。
| 基準 | Fake | Stub | Mock |
|---|---|---|---|
| ロジックの有無 | あり(簡略化) | なし | なし |
| 速度 | 高速 | 最速 | 高速 |
| 動作検証 | 間接的 | なし | あり(verify) |
| メンテナンス | インターフェースごとに1クラス | テストごとに設定 | テストごとに設定 |
| 現実性 | 高い(コードが動作する) | 低い(固定データ) | 中程度 |
| 偽陽性のリスク | 低い | 中程度 | 高い(脆弱なテスト) |
アンチパターン: Fakeではないfake — 開発者が実際にはstubやmockであるオブジェクトをfakeと呼ぶよくある間違い。InMemoryUserRepositoryにロジック(フィルタリング、ソート)がない場合、それはfakeではなく、インメモリストレージ付きのstubです。Fakeがstubと異なる点は、まさに実行可能なロジックの存在です。fakeリポジトリが単に格納されたものを返すだけでデータを処理しない場合は、mockまたはstubを使用してください。
実践的な推奨 — すべてのリポジトリまたはサービスに対してfakeから始めてください。Fakeが50行を超える場合は、複数のクラスに分割してください。Fakeがまったく必要ない場合(テストが固定データを使用して単一のシナリオのみを検証する場合)は、stubを使用してください。テストが特定のパラメータでメソッドが呼び出されたことを検証する場合は、mockを使用してください。事前に選択を最適化しないでください。fakeを作成し、それが過剰であることが判明した場合は、特定のテストでstubに置き換えてください。
Room向けFakeリポジトリ — Androidでの典型的なfakeの例。UserRepositoryの本番実装はSQLiteクエリとともにRoom DAOを使用します。FakeバージョンはMutableListまたはHashMapにデータを保存し、同じメソッド(getUser(id)、saveUser(user)、deleteUser(id))を実装します。Fakeには検索、フィルタリング、ソートのロジックが含まれています。本番リポジトリと同じですが、SQLはありません。これにより、Roomデータベースを設定せずにViewModelとUseCaseをテストできます。
class FakeUserRepository : UserRepository {
private val users = mutableListOf<User>()
override suspend fun getUser(id: String): User? {
return users.find { it.id == id }
}
override suspend fun saveUser(user: User) {
val index = users.indexOfFirst { it.id == user.id }
if (index >= 0) users[index] = user
else users.add(user)
}
override suspend fun search(query: String): List<User> {
return users.filter {
it.name.contains(query, ignoreCase = true)
}
}
}
Retrofit API向けFake — MockWebServer(stubでありfakeではない)の代わりに、インメモリコレクションからデータを返すApiService実装を作成できます。違い:MockWebServerはHTTPをインターセプトしてJSONを返しますが、fake ApiServiceはシリアライゼーションなしでKotlinインターフェースレベルで動作します。Fakeはより高速で(JSON解析なし)、デバッグが容易です(同じプロセスで実行され、型付けされています)。HTTPセマンティクス(ステータスコード、ヘッダー)が重要でないテストに適しています。
SwiftでのFake — プロトコルを通じて構築されます。本番クラスは実際のロジック(CoreData、URLSession)でプロトコルを実装します。Fake構造体はインメモリストレージと簡略化されたロジックで同じプロトコルを実装します。Swiftは値セマンティクスの言語であるため、fake構造体は不変であり、マルチスレッドテストでも安全です。これにより、Androidの類似物よりも優位性があります。インメモリデータへのアクセスを同期する必要がありません。
protocol UserRepositoryProtocol {
func getUser(id: String) async -> User?
func saveUser(user: User) async
}
final class FakeUserRepository: UserRepositoryProtocol {
private var storage: [String: User] = [:]
func getUser(id: String) async -> User? {
return storage[id]
}
func saveUser(user: User) async {
storage[user.id] = user
}
}
final class UserViewModelTests: XCTestCase {
func test_save_and_load() async {
let fake = FakeUserRepository()
let vm = UserViewModel(repository: fake)
let user = User(id: "1", name: "Alice")
await vm.saveUser(user)
let loaded = await vm.getUser(id: "1")
XCTAssertEqual(loaded?.name, "Alice")
}
}
CoreData向けFake — iOSプロジェクトでは、description.type = NSInMemoryStoreTypeを設定してインメモリのNSPersistentContainerを作成できます。これは完全なCoreDataスタックですが、メモリ内で動作します。このようなfakeを使用すると、SQLiteファイルを作成せずにNSFetchRequest、述語、ソートをテストできます。速度:インメモリCoreDataでのテストは、ディスクベースの類似物よりも5〜10倍高速に実行されます。欠点:毎回NSManagedObjectModelを設定する必要があります。
FakeURLProtocol — iOSでネットワークリクエストをインターセプトするためのURLProtocolのサブクラスです。URLProtocol.registerClass(fakeProtocol)を介して登録されます。内部にはインメモリのURL -> Data辞書があり、実際のリクエストなしでデータを返します。Stubとの違い:FakeURLProtocolはリクエスト本文、ヘッダーをチェックし、入力データに応じて異なるレスポンスを返すことができます。リクエストルーティングロジックを含むため、これはfakeです。
Test FixtureとしてのFake — fakeクラスを共有テストモジュール(androidTest/sharedTestまたはTestSupport)に配置します。プロジェクトのすべてのテストが同じInMemoryUserRepositoryを使用します。これにより、各テストでのモックオブジェクト設定の重複が排除され、統一された動作が保証されます。Fakeのロジックを変更すると、すべてのテストが同時に更新されます。IT Sectrでは、fakeクラスをsharedTest/java/com/itSectr/fake/に保存し、implementation project(:sharedTest)を介して組み込んでいます。
事前設定データを持つFake — テストでは、すでにいくつかのレコードを含むリポジトリが必要になることがよくあります。解決策:ファクトリメソッドfakeWithData(vararg items)または組み込みのaddDefaultData()メソッド。ファクトリはfakeを作成し、典型的なデータを入力して、使用可能なオブジェクトを返します。これにより、テストのボイラープレートが削減されます。モック呼び出しを設定する代わりに、テストは単にFakeUserRepository.withUsers(alice, bob)を呼び出します。
呼び出しカウント付きFake — 状態だけでなく呼び出し回数も検証する必要がある場合があります。Fakeにはカウンター(saveCallCount、getUserCallCount)を含めることができます。テストは実行後にカウンターをチェックします。これは純粋なfake(状態検証)とmock(相互作用検証)の間の妥協点です。カウンターは引数や呼び出し順序をチェックせず、回数のみをチェックします。引数の検証にはmockを使用してください。
Callback付きFake — 非同期シナリオをテストするために、fakeは各呼び出しでコールバック(beforeGetUser、afterSaveUser)を受け入れることができます。これにより、遅延やエラーのシミュレーション、中間状態のチェックが可能になります。このアプローチはUIのローディング状態のテストに役立ちます。fakeが100ミリ秒一時停止し、テストが画面にローダーが表示されていることを確認します。コールバックは本番環境にはありません。これは純粋にテスト機能です。
よくある質問
Fakeには機能的なロジック(フィルタリング、ソート、カウント)が含まれています。Stubはロジックなしで所定の回答のみを返します。オブジェクトに分岐(if/else、when)がある場合はfakeです。戻り値のみを含む場合はstubです。Fakeはメンテナンスコストが高いですが、より現実的なテストを提供します。
Fakeのロジックが本番のロジックと一致しない場合です。例えば、FakeUserRepositoryが大文字小文字を区別する検索を使用し、本番バージョンが大文字小文字を区別しない場合。テストは合格しますが、実際にはバグがあります。解決策:fakeのロジックを個別にテストするか、単純なロジック(CRUD操作)を持つインターフェースにのみfakesを使用します。複雑なロジックの場合は、実際のデータベースを使用した統合テストを作成してください。
インメモリデータベースはfakeの一種です。Room.inMemoryDatabaseBuilder()は本番データベースのように動作するインメモリSQLiteを作成します。これは本格的なfakeです。しかし、fakeはリポジトリレベル(SQLなし)やネットワークレベル(FakeApiService)でも存在できます。インメモリデータベースは、ロジックが可能な限り実際のものに近いfakeの特殊なケースです。
はい、ただし注意が必要です。Fakeはリポジトリ(データ)用、MockはAnalyticsTracker(イベント検証)用です。レイヤーごとの分離:データレイヤーにはfake、分析/ロギングレイヤーにはmock。1つのオブジェクトを同時にfakeとmockにしないでください。これは単一責任の原則に違反し、テストを混乱させます。
Fakeをテストするには、本番実装と同じテストを使用します。save、get、deleteを検証するUserRepositoryTestがある場合、FakeUserRepositoryとRealUserRepositoryの両方でテストを実行します。これにより、fakeが本番クラスの動作を再現することが保証されます。Fakeが異なる動作を始めた場合、両方の実装でテストが失敗します。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。