Stub(スタブ、テストドゥーブル)— 実装の代わりにメソッド呼び出しに対して事前定義された応答を返すテストオブジェクトです。モバイル開発において、スタブはテスト対象モジュールをネットワークリクエスト、データベース、ファイルシステムからアイソレートし、環境設定なしでロジックを検証できるようにします。Mockとは異なり、Stubは行動を検証しません—単にデータを提供するだけです。詳細は、Martin Fowlerのテストドゥーブルに関する記事をご覧ください。
ポイント
Stubは、テストにおいて実際の依存関係を置き換え、特定の呼び出しに対して事前定義値を返すテストドゥーブルオブジェクトです。この用語は、Gerard Meszaros(2007)による分類で、書籍『xUnit Test Patterns』に提案されました。Stubはテストドゥーブルのカテゴリに属します—テスト中に実際のコンポーネントを代替するオブジェクトです。スタブの主な目的は、テスト対象ユニットに予測可能なデータを提供し、外部システムの不確実性を削除することです。
仕組み—テストは実行前にスタブを構成します:「getUsers()メソッドが呼ばれたら、このユーザーリストを返せ」。Stubにビジネスロジックはなく、呼び出し順序や呼び出し履歴を検証しません。単に実際のコンポーネントの代わりに立ち、指示されたものを返すだけです。Androidテストの文脈では、OkHttpクライアントはサーバーに実際のリクエストを送らず、スタブとして構成されたMockWebServerから応答を受け取ります。
使用すべきタイミング—スタブはUI層(ViewModel、Presenter)とビジネスロジック(UseCase、Interactor)のテストに最適です。特定のデータに対する反応を検証する必要がある場合(空のリスト、サーバーが500エラーを返した、トークンが期限切れ)など。テストが特定の入力状態を必要とする場合は、すべてスタブの役割です。テストシナリオごとにスタブの構成が用意され、テストの可読性と予測可能性が向上します。
Gerard Meszaros(2007)は、書籍『xUnit Test Patterns』で、テストドゥーブルを五つのタイプに分類しました:dummy、stub、spy、mock、fake。各タイプはそれぞれの問題を解決します。Dummy—渡されるが使用されません。Stub—データを返します。Spy—呼び出しを記録します。Mock—行動を検証します。Fake—簡素化されたロジックを含みます。この分類を理解することで、開発者はシナリオごとに適切なツールを選べるようになります。
ネットワークリクエスト—スタブの最も一般的な使用シナリオです。アプリはAPIにHTTP呼び出しを行い、テストではさまざまな応答への反応を検証する必要があります:成功したJSON、401エラー(未認証)、タイムアウト、空の配列。AndroidのMockWebServer(OkHttp)やURLProtocol(iOS)は、実際のサーバー接続なしで事前定義されたHTTP応答を返すスタブとして機能します。これにより、テストを秒からミリ秒まで加速できます。
データベース—Room(Android)とCoreData(iOS)にはインメモリバリアントがありますが、セットアップにまだ時間がかかります。リポジトリの代わりにStubを使うと、データベースを触らずに事前用意されたEntityリストを返せます。これは、ソート、フィルタリング、データ変換を検証する必要があるViewModelのテストに特に効果的です。テストはデータ量に関係なくミリ秒で実行されます。
システムサービス—LocationManager、SensorManager、SharedPreferencesは実機またはエミュレーターが必要です。LocationProviderのスタブは事前定義された坐標を返し、SensorManagerのスタブは事前定義された加速度センサー値を返します。iOSの場合、同槻は、テストデリゲート実装を持つCLLocationManagerです。スタブがない場合、これらのテストには特定の条件を持つ実機が必要になります。
ファイルシステムとキャッシュ—画像の読み込み、応答のキャッシュ、設定ファイルの扱いは、すべてディスクの状態に依存します。FileManagerやImageCacheのスタブは、実際のファイルを読み込まずに成功/エラーを返します。これにより、開発胅の異なるマシンでのパス不一致やアクセス権限による仮のテスト失敗が解消されます。
責任の分担—3つのタイプのテストドゥーブルは、それぞれ異なる課題を解決します。Stub:「データをくれ」。Mock:「呼び出されたか検証して」。Fake:「実際のものと同じように動くが、より簡素」。この違いはテストの可読性に致命的です:スタブが必要な場所でmockを使用すると、テストはシナリオに関係のないverify呼び出しで満たされます。
| 特徴 | Stub | Mock | Fake |
|---|---|---|---|
| 目的 | データを提供 | 相互作用を検証 | 簡素化実装 |
| ロジック | なし | なし | あり(簡素化) |
| 検証 | なし | あり(verify) | 間接的(状態を通じて) |
| 柔軟性 | 低|固定応答 | 中 | 高|ロジックが適応 |
| 速度 | 最大 | 高 | 中 |
| 例 | MockWebServerがJSONを返す | Mockito.verify(repository).save() | HashMapを使用するInMemoryRepository |
実践的なルール—テストがテスト対象コンポーネントが受け取ったデータを検証する場合は、スタブを使用します。テストが、コンポーネントが正しい引数で依存関係のメソッドを呼んだかを検証する場合は、mockを使用します。単にデータベースをハッシュテーブルで置き換えたい場合—それはfakeです。タイプを一つのテストで混乱させると、テストは脆弱になります:実装が変わった場合、スタブとverifyの両方のロジックを書き直す必要があります。
Stubとverifyの組み合わせ—開発者がスタブを設定し、その後にverify(stub).method()を追加する一般的な誤りです。Stubは定義上、検証されるべきではありません—検証にはMockがあります。特定の引数でメソッドが呼ばれたかを検証する必要がある場合は、Mockito.stub()ではなくMockito.mock()を使用します。この区切りにより、テストの目的が他の開発者に対して明確になります。
MockWebServer—AndroidおよびJVM上でHTTPスタブを作成するためのOkHttpライブラリです。指定されたポートでローカルHTTPサーバーを起動し、OkHttpクライアントのリクエストを拦って事前定義された応答を返します。設定は3行で完了します:サーバーを作成、応答をエンキュー、起動。テストは、ページネーションやリトライのシナリオに対して、複数の応答を順次エンキューできます。
class UserRepositoryTest {
private val server = MockWebServer()
fun setup() {
server.start(8080)
val client = OkHttpClient.Builder()
.readTimeout(1, TimeUnit.SECONDS)
.build()
}
fun test_user_list_success() {
val json = "[{\"id\":1,\"name\":\"Alice\"}]"
server.enqueue(MockResponse()
.setBody(json)
.setResponseCode(200)
)
val result = repository.getUsers()
assertEquals(1, result.size)
}
fun teardown() {
server.shutdown()
}
}
MockK—コルーチン、拡張関数、sealedクラスをファーストクラスでサポートするKotlin用のMockitoの代替品です。MockKのスタブは、coEvery(suspend関数用)とevery(通常の関数用)を通じて作成されます。MockWebServerとは異なり、MockKはHTTP層全体ではなく、個々の依存関係メソッドをスタブします。これは、依存関係がリポジトリの抽象であるUseCaseやInteractorの単体テストに便利です。
interface UserRepository {
suspend fun getUsers(): List<User>
}
class GetUsersUseCaseTest {
private val repo = mockk<UserRepository>()
private val useCase = GetUsersUseCase(repo)
fun test_empty_list() = runTest {
coEvery { repo.getUsers() } returns emptyList()
val result = useCase.invoke()
assertTrue(result.isEmpty())
coVerify(exactly = 1) { repo.getUsers() }
}
}
最良プラクティス—統合テストにはMockWebServer(実際のHTTPを拦截)を、単体テストにはMockK(インターフェースをスタブ)を使用します。テストしていないものをスタブしないでください:テストがRepositoryを検証する場合、その内部のOkHttpクライアントをスタブせずに、HTTPレベルで実際のMockWebServerを使用します。このルールにより、テストの適切さが保たれ、リファクタリング時の脆弱性が減少します。
Swiftプロトコルをスタブとして—iOSネイティブのアプローチでは、依存関係プロトコルに準拠するテスト構造体を代入することでスタブを実装します。実際のNetworkServiceの代わりに、テストは固定データを返すStubNetworkServiceを受け取ります。Swiftは静的型付け言語であるため、スタブは実際のサービスと同じプロトコルに準拠する必要があります。コンパイラがスタブが必要なメソッドをすべて実装していることを保証します。
protocol NetworkServiceProtocol {
func fetchUsers() async throws -> [User]
}
struct StubNetworkService: NetworkServiceProtocol {
let result: Result<[User], Error>
func fetchUsers() async throws -> [User] {
try result.get()
}
}
final class UsersViewModelTests: XCTestCase {
func test_success_state() async {
let stub = StubNetworkService(
result: .success([User(name: "Alice")])
)
let vm = UsersViewModel(service: stub)
await vm.load()
XCTAssertEqual(vm.users.count, 1)
}
}
Objective-C用OCMock—レガシーiOSプロジェクトでスタブやモックを作成するためのライブラリです。OCMockは、引数や戻り値を持つスタブメソッドをサポートしています。現代のSwiftプロジェクトは、マニュアルスタブを伴うプロトコルベースのアプローチを好みます:これにより各メソッドを制御でき、外部依存関係が不要になります。OCMockは、すべての依存関係をプロトコル化することが財務的に現実的でないプロジェクトにとって選択肢として残っています。
HTTPスタブのためのURLProtocol—URLProtocolのサブクラスを通じてネットワークリクエストを拦截する、iOSのシステムメカニズムです。テストは、URLSessionを拦ってスタブ応答を返すカスタムURLProtocolを登録します。マニュアルスタブに対する利点:アプリのアーキテクチャを変える必要がありません—URLSessionは実際のままで、データがプロトコルレベルで代替されます。缺点:明示的なスタブサービスよりデバッグが困難です。
よくある質問
Stubは事前定義データを返し、呼び出しが行われたかどうかは検証しません。Mockはさらに、メソッドが正しい引数で呼ばれたことを検証します(verify)。Stubは「何を返すか」という問いに答え、Mockは「呼び出しは行われたか」に答えます。状態検証にはStubを、相互作用検証にはMockを使用してください。
Fakeは、テストが動作する(簡素でも)実装を必要とする場合に使用します—例えば、Roomの代わりにインメモリデータベース。Stubは、事前定義データを持つ単一シナリオに適しています。同じStubを10つのテストで繰り返し使用している場合は、多分Fakeが必要です。Fakeは、ロジックが卒一のクラスにまとまっているため、デュプリケーションが減ります。
Androidでは—MockKはKotlinオブジェクト(object)のmockkObject()をサポートし、mockkStatic()を通じてJavaクラスの静的メソッドを含みます。iOSでは—Swiftの静的メソッドは直接スタブできません。プロトコルとDIを使用して、static呼び出しをプロトコルのインスタンスメソッドに置き換えます。静的スタブは技術負債であり、新規コードでは避けるべきです。
MockWebServer(OkHttp)を使用します—これは、応答をエンキューするローカルHTTPサーバーとして動作します。Retrofitの場合は、ベースURLをlocalhost:8080に変更するだけです。Ktorの場合は、MockEngineを使用します—HttpStatementを代替する組み込みメカニズムです。どちらのアプローチも実際のインターネットなしで動作し、ステータスコード、ボディ、応答ヘッダーを完全に制御できます。
Spyは実際のオブジェクトをラップして呼び出しを記録するのに対し、Stubはオブジェクトを固定応答で完全に置き換えます。Spyは実際の実装を一部的に使用できますが(他のメソッドはそのまま動作)、Stubはできません。メソッドが呼ばれたことを検証したいが、ロジックの一部は実行される必要がある場合—StubではなくSpyを使用してください。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。