Spy(スパイ)— 実際のインスタンスをラップし、各呼び出しに関する情報(どのメソッドが呼び出されたか、どの引数で、何回か)を記録するテストオブジェクトです。mockとは異なり、spyはラップされたオブジェクトの実際の実装を使用します—呼び出しは実際のコードを通過し、spyは事実を記録するだけです。テスト実行後、開発者はスパイの記録を確認します:「sendAnalyticsメソッドは3回呼び出されましたか?」。詳細はAndroidテストガイドをご覧ください。
重要なポイント
Spy — 実際のオブジェクトの周りのラッパーで、すべてのメソッド呼び出しをインターセプトして記録します。オブジェクトの実際のロジックは実行されます:メソッドがデータを保存したり、値を計算したり、リクエストを行ったりする場合—すべて通常通り行われます。さらに、spyはメタデータ(メソッド名、引数、呼び出し回数、実行時間)を記録します。この用語はMeszaros(2007)の分類の一部であり、Martin Fowlerの記事「Mocks Aren't Stubs」で詳細に説明されています。
Mockとの主な違い — mockはオブジェクトをテスト用スタブで完全に置き換えます。すべてのメソッドはデフォルトで何も行いません。Spyは既存のオブジェクトをラップします:すべてのメソッドはデフォルトで通常通り動作しますが、記録もされます。この違いは基本的です:mockはテスト対象のコードを現実から隔離し、spyは現実を維持して観察できるようにします。どちらを選ぶかは、何をテストしているかによります。
Spyが適切な選択 — テスト対象のコードが実際のオブジェクトの状態を変更し、テストが結果(状態)の確認と、呼び出しが正しい順序で行われたことの確認の両方を行う必要がある場合。mockは実際の実装を実行しないため適していません。stubは呼び出しを記録しないため適していません。Spyは、実際のロジックを維持しながら呼び出しに関する情報を提供する唯一のテストダブルです。
Mock — 完全な隔離。テストが実際のオブジェクトの実装に依存すべきでない場合(例:データベースやネットワーククライアント)、mockを使用します。mockは呼び出しが実際のコンポーネントに到達しないことを保証します。安全で予測可能です。欠点:mockは実際のロジックを実行しないため、テスト対象のコードが戻り値に依存する場合は、when/stubで明示的に設定する必要があります。
Spy — 実際のロジック + 観察。テスト対象のコードがデータだけでなく、そのロジックがテストにとって重要なオブジェクトと相互作用する場合—spyを使用します。例えば、イベントを収集して定期的に送信するAnalyticsTracker。テストはイベントがバッファに追加されたこと、送信後にバッファがクリアされたことを確認します。mockはトラッカーの実際のロジックを実行しないため、これを確認できません。
| シナリオ | Spy | Mock |
|---|---|---|
| 実際のロジックが必要 | はい | いいえ(stub) |
| 呼び出し検証 | はい(回数、引数) | はい(回数、引数) |
| 部分的なスタブ化 | はい(一部のメソッドはspy、他はstub) | いいえ(すべてのメソッドがstub) |
| 副作用のリスク | 高い(実際のコード) | ゼロ |
| 速度 | 低い(実際のロジック) | 高い(stub) |
| 可読性 | 低い(何が実際か理解しにくい) | 高い(すべて明示的) |
すべてにspy — すべてのテストでmockの代わりにspyを使用するのは誤りです。spyは実際のコードを実行し、ファイルへの書き込み、HTTP送信、グローバル状態の変更などの副作用がある可能性があります。テスト対象のモジュールがHTTPリクエストを行うspyオブジェクトのメソッドを呼び出す場合、テストは単体テストではなく統合テストになります。ルール:spyがI/O操作のあるオブジェクトをラップする場合—それはもはや単体テストではありません。I/Oを隔離するにはmockを使用し、spyは外部効果のないインメモリオブジェクトにのみ使用してください。
Mockito.spy() — Java/Kotlinプロジェクトでスパイを作成する古典的な方法。spy()は実際のオブジェクトを受け取り、ラッパーを返します。すべての呼び出しはデフォルトで実際のオブジェクトに委任され、結果が記録されます。テスト後、verify()を使用して呼び出し回数と引数を確認できます。テストデータを返すべきメソッドにはdoReturn/whenを使用します—これは「部分モック」と呼ばれます。
class AnalyticsReporterTest {
private val realTracker = AnalyticsTracker()
private val spyTracker = Mockito.spy(realTracker)
fun test_event_tracked() {
val event = AnalyticsEvent("login")
spyTracker.track(event)
Mockito.verify(spyTracker).track(event)
assertEquals(1, spyTracker.getBufferedCount())
}
fun test_track_with_exception() {
Mockito.doThrow(RuntimeException("network"))
.when(spyTracker).flush()
spyTracker.track(AnalyticsEvent("login"))
assertTrue(spyTracker.hasPendingEvents())
}
}
MockK.spyk() — コルーチンとsealedクラスのサポートが向上したKotlinプロジェクト向けの代替手段。MockK.spyk()はMockito.spy()に相当するスパイを作成します。suspend関数用のcoVerifyと部分的なスタブ化用のeveryをサポートしています。Mockitoとは異なり、MockKはfinalクラスのspyをサポートしていません(Kotlinのすべてのクラスはデフォルトでfinal)—クラスをopenにするかインターフェースを使用する必要があります。
class LoginUseCaseTest {
private val realRepo = UserRepository()
private val spyRepo = spyk(realRepo)
private val useCase = LoginUseCase(spyRepo)
fun test_login_calls_save() = runTest {
every { spyRepo.getUser(any()) } returns User("test")
val result = useCase.login("test", "pass")
coVerify { spyRepo.saveLoginTime(any()) }
assertTrue(result.isSuccess)
}
}
部分的なスタブ化 — 強力だが危険なテクニック。オブジェクトのspyを作成し、一部のメソッドのみを上書き(stub)して、残りを実際のままにすることができます。例:getUser()がテストデータを返し、saveUser()が実際にインメモリリストに保存するspyリポジトリ。これにより、stub(制御されたデータ)とspy(実際のロジック)の両方の利点を組み合わせることができます。欠点:テストの可読性が低下します—どのメソッドが実際で、どれがstubかが明確ではありません。
Objective-C用のOCMock — niceMockを介してspyオブジェクトの作成をサポートするライブラリ。OCMockはObjective-Cランタイムを使用してメソッド呼び出しをインターセプトし、記録します。テスト後にverifyが呼び出されます。OCMockは任意のオブジェクトのspyをサポートしており(Objective-Cのすべてのメソッドは動的)、spyがプロトコルを介してのみ可能なSwiftよりも優位性があります。
// 実際のオブジェクトのspyを作成
AnalyticsTracker *realTracker = [[AnalyticsTracker alloc] init];
AnalyticsTracker *spy = [OCMockObject partialMockForObject:realTracker];
// テストの実行
[spy trackEvent:@"login"];
// 検証
[[spy verify] trackEvent:@"login"];
XCTAssertEqual([realTracker eventCount], 1);
Swiftプロトコルベースのspy — SwiftにはObjective-Cのようなランタイムリフレクションがないため、spyは手動で作成されます。テスト構造体がプロトコルを実装し、内部で実際のオブジェクトを呼び出しながら同時に呼び出しを記録します。これにはより多くのコードが必要ですが、完全に制御可能で型安全です。手動スパイは外部ライブラリを必要とせず、ランタイムも使用しません—すべてコンパイル時にチェックされます。
protocol AnalyticsProtocol {
func trackEvent(name: String)
}
final class SpyAnalytics: AnalyticsProtocol {
private let real: AnalyticsProtocol
private var events: [String] = []
init(real: AnalyticsProtocol) {
self.real = real
}
func trackEvent(name: String) {
events.append(name)
real.trackEvent(name: name)
}
func verifyTracked(name: String) -> Bool {
return events.contains(name)
}
}
OCMockと手動spyの使い分け — Objective-CコードにはOCMockを使用します(ボイラープレートが少ない)。Swiftにはプロトコルを介した手動スパイが推奨されます。手動spyは呼び出し記録を完全に制御でき、リフレクションを必要とせず、値型(struct)でも動作します。唯一の欠点は、新しいメソッドを追加する際にspyクラスのコードをプロトコルと同期させておく必要があることです。
アナリティクスの検証 — スパイの最も一般的な使用ケース。プロダクションコードでは、アナリティクス呼び出しはアプリケーション全体に散らばっています:ログイン、ログアウト、購入、エラー。テストはAnalyticsTrackerのspyラッパーを作成し、シナリオ(ログイン、商品閲覧、カート追加、購入)を実行し、必要なすべてのイベントが正しい順序で送信されたことを確認します。AnalyticsTrackerにはバッファリングと送信ロジックが含まれているため、mockは適していません。
タイマーとスケジューラ — Handler(Android)やTimer(iOS)を使用するコードのテストは、リアルタイムのために困難です。Schedulerのspyラッパーは、どのタスクがどの遅延でスケジュールされたかを記録します。テストは実際のHandlerのspyを作成し、アクションを実行し、Handler.postDelayed(runnable, delay)が正しい遅延で呼び出されたことを確認します。実際のタスクは実行されません—spyが呼び出しをインターセプトして記録します。
ロギングとデバッグ情報 — プロダクションでは、ログが無効になっているかファイルに書き込まれている場合があります。Loggerのspyラッパーは、すべてのメッセージをインメモリリストに記録し、テストが実行後に確認します。これにより、コンソールを散らかすことなく、エラー時に正しいメッセージが書き込まれたことを確認できます。Loggerの手動スパイは、OSLogにテストAPIがないiOSで特に便利です。
呼び出し順序の検証 — 一部のシナリオでは厳密な操作順序が必要です:接続を開く、データを送信する、接続を閉じる。MockitoはInOrder.verify()を介して順序の確認を可能にします。spyも同じことを行いますが、実際の実行を維持します。順序と各ステップの結果(接続が実際に開かれたこと)の両方が重要な場合—mockではなくspyを使用してください。
よくある質問
Spyは実際のオブジェクトをラップし、そのロジックを実行しながら呼び出しを記録します。Mockはオブジェクトを完全にスタブに置き換えます—実際のロジックは実行されません。spyは振る舞いを維持し、mockは維持しません。オブジェクトの実際の動作が重要な場合はspyを選択してください。外部依存関係からテストを隔離する必要がある場合はmockを選択してください。
spyラッパーが実際のI/O操作につながる場合。spyがファイルに書き込む、HTTPを送信する、ディスクから読み取るオブジェクトをラップする場合—テストは単体テストではなくなります。2つ目のケース:テストが呼び出しに関心を持たずに戻り値のみを確認する場合—ここではstubで十分で、spyは冗長です。3つ目:コードがspyの内部状態に依存する場合—これは脆弱なテストです。
はい、spyk()を介して—Mockito.spy()に相当します。MockK.spyk()は実際のオブジェクトの周りにスパイを作成し、部分的なスタブ化にはevery、suspend関数にはcoVerify/coroutinesVerifyをサポートしています。制限:finalクラスでは動作しません(openまたはインターフェースが必要)。Javaクラスの場合、MockKもspyk()をサポートしていますが、@MockKJvmInlineアノテーションが必要です。
技術的には、いいえ。Mockは実際の実装を含まないスタブです。Spyは定義上、実際のオブジェクトをラップします。Mockitoでは、mockをspyに変換することはできません。しかし、逆は可能です:spyを作成し、doReturn/when(部分モック)を介していくつかのメソッドを上書きします。これにより、spyオブジェクトの選択されたメソッドに対してmockのような動作が得られます。
はい、必要です。SwiftにはJava/Kotlinのような動的プロキシはありません。spyを作成するには、プロダクションクラスとspyクラスの両方が実装するプロトコルが必要です。Swiftのプロトコルベースのspyは、実際のオブジェクトを受け取り、呼び出しを委任し、メタデータを記録する手動実装です。代替手段:SourceKitを介してspyクラスを生成するCuckooライブラリ。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。