テストコード内に散在しているモック定義を一箇所に集約し、以下の問題を解決します:
- ❌ 同じRepositoryのモックが複数のテストファイルに重複定義されている
- ❌ モックの存在を知らずに新しいメンバーがローカルで再定義してしまう
- ❌ モックの仕様変更時に複数箇所を修正する必要がある
パターン1: Mocks列挙型を採用します。このパターンは:
- ✅ 既存のStore/ViewModelコードを変更不要
- ✅ 今日から段階的に移行可能
- ✅ 学習コストが低い
- ✅ DependencyProtocolへの将来的な移行も可能
テストターゲット内に Mocks.swift ファイルを作成します:
import Foundation
@testable import YourApp
/// すべてのテスト用モック実装を集約する列挙型
/// - Note: 新しいモックを追加する際は必ずここに定義すること
enum Mocks {
// この中にすべてのモック実装を配置
}配置場所例:
YourAppTests/
├── Mocks/
│ └── Mocks.swift # ← ここに作成
├── UserStoreTests.swift
└── RoomStoreTests.swift
各テストファイルに散在しているモック定義を Mocks 列挙型の中に移動します。
// UserStoreTests.swift
class MockUserRepository: UserRepositoryProtocol {
var stubbedUser: User?
func fetch() async throws -> User {
return stubbedUser ?? User(id: 1, name: "Test")
}
}
class UserStoreTests: XCTestCase {
func testFetchUser() async throws {
let mock = MockUserRepository()
// ...
}
}// ProfileViewTests.swift
// 同じモックが重複定義されている!
class MockUserRepository: UserRepositoryProtocol {
var stubbedUser: User?
func fetch() async throws -> User {
return stubbedUser ?? User(id: 1, name: "Default")
}
}// Mocks.swift
enum Mocks {
class UserRepository: UserRepositoryProtocol {
var stubbedUser: User?
var fetchCallCount = 0 // 呼び出し回数検証用
func fetch() async throws -> User {
fetchCallCount += 1
return stubbedUser ?? User(id: 1, name: "Test User")
}
}
}// UserStoreTests.swift
class UserStoreTests: XCTestCase {
func testFetchUser() async throws {
let mock = Mocks.UserRepository() // ← Mocks配下から取得
mock.stubbedUser = User(id: 1, name: "Test")
let store = UserStore(repository: mock)
try await store.loadUser()
XCTAssertEqual(mock.fetchCallCount, 1)
}
}// ProfileViewTests.swift
class ProfileViewTests: XCTestCase {
func testShowProfile() async throws {
let mock = Mocks.UserRepository() // ← 同じモックを再利用
mock.stubbedUser = User(id: 2, name: "Alice")
// ...
}
}プロジェクト内のすべてのRepositoryに対応するモックを Mocks 配下に定義します:
// Mocks.swift
enum Mocks {
// MARK: - Repositories
/// ユーザー情報取得用モック
class UserRepository: UserRepositoryProtocol {
var stubbedUser: User?
var fetchCallCount = 0
func fetch() async throws -> User {
fetchCallCount += 1
return stubbedUser ?? User(id: 1, name: "Test User")
}
}
/// ルーム一覧取得用モック
class RoomRepository: RoomRepositoryProtocol {
var stubbedRooms: [Room] = []
var fetchListCallCount = 0
func fetchList() async throws -> [Room] {
fetchListCallCount += 1
return stubbedRooms
}
}
/// メッセージ取得用モック
class MessageRepository: MessageRepositoryProtocol {
var stubbedMessages: [Message] = []
var fetchMessagesCallCount = 0
func fetchMessages(roomId: Int) async throws -> [Message] {
fetchMessagesCallCount += 1
return stubbedMessages
}
}
// ... 他のRepository
}既存のテストファイルを順次更新していきます:
// RoomStoreTests.swift
class RoomStoreTests: XCTestCase {
func testFetchRoomList() async throws {
// 1. Mocks配下からモックを取得
let mock = Mocks.RoomRepository()
// 2. テストデータをスタブ
mock.stubbedRooms = [
Room(id: 1, name: "General"),
Room(id: 2, name: "Random")
]
// 3. Storeにモックを注入
let store = RoomStore(repository: mock)
// 4. テスト実行
try await store.loadRooms()
// 5. 検証
XCTAssertEqual(mock.fetchListCallCount, 1)
XCTAssertEqual(store.rooms.count, 2)
}
}class UserRepository: UserRepositoryProtocol {
// ✅ Good: stubbed[型名]
var stubbedUser: User?
var stubbedError: Error?
// ❌ Bad: 曖昧な命名
var result: User?
var testData: User?
}class UserRepository: UserRepositoryProtocol {
// ✅ Good: [メソッド名]CallCount
var fetchCallCount = 0
var updateCallCount = 0
func fetch() async throws -> User {
fetchCallCount += 1
// ...
}
func update(user: User) async throws {
updateCallCount += 1
// ...
}
}class MessageRepository: MessageRepositoryProtocol {
var fetchMessagesCallCount = 0
var capturedRoomIds: [Int] = [] // ✅ 渡された引数を記録
func fetchMessages(roomId: Int) async throws -> [Message] {
fetchMessagesCallCount += 1
capturedRoomIds.append(roomId)
return stubbedMessages
}
}
// テストでの利用
func testFetchMessagesWithCorrectRoomId() async throws {
let mock = Mocks.MessageRepository()
let store = MessageStore(repository: mock)
try await store.loadMessages(roomId: 123)
XCTAssertEqual(mock.capturedRoomIds, [123]) // 正しいIDが渡されたか検証
}class UserRepository: UserRepositoryProtocol {
var stubbedUser: User?
var stubbedError: Error? // ✅ エラーをスタブ可能に
func fetch() async throws -> User {
if let error = stubbedError {
throw error
}
return stubbedUser ?? User(id: 1, name: "Test")
}
}
// テストでの利用
func testHandleNetworkError() async throws {
let mock = Mocks.UserRepository()
mock.stubbedError = NetworkError.timeout // エラーをスタブ
let store = UserStore(repository: mock)
do {
try await store.loadUser()
XCTFail("Should throw error")
} catch {
XCTAssertTrue(error is NetworkError)
}
}-
Mocks.swiftファイルを作成 - すべてのRepositoryプロトコルに対応するモックを
Mocks配下に定義 - 各テストファイル内の重複モック定義を削除
- すべてのテストが
Mocks.[Repository名]を使用するよう更新 - CI/CDが正常に動作することを確認
- チームメンバーに新しいルールを共有
- 新しいモックは必ず
Mocks.swiftに追加 - ローカルでモックを定義しない
- 新しいモックは必ず
A: モック定義を Mocks 配下に移動する際、プロパティ名やメソッド名が変わっていないか確認してください。
// Before
class MockUserRepo: UserRepositoryProtocol { ... }
// After
class UserRepository: UserRepositoryProtocol { ... } // ← クラス名が変わった
// テストの修正
// let mock = MockUserRepo() // Before
let mock = Mocks.UserRepository() // AfterA: Mocks.swift のモッククラスを直接編集してください。変更は自動的にすべてのテストに反映されます。
// Mocks.swift
enum Mocks {
class UserRepository: UserRepositoryProtocol {
var stubbedUser: User?
var fetchCallCount = 0
var shouldSimulateSlowNetwork = false // ← 新機能追加
func fetch() async throws -> User {
fetchCallCount += 1
if shouldSimulateSlowNetwork {
try await Task.sleep(nanoseconds: 2_000_000_000)
}
return stubbedUser ?? User(id: 1, name: "Test")
}
}
}A: はい、推奨します。同じパターンで Mocks.Store を追加できます:
enum Mocks {
// MARK: - Repositories
class UserRepository: UserRepositoryProtocol { ... }
// MARK: - Stores
class UserStore: UserStoreProtocol {
var stubbedUser: User?
var loadUserCallCount = 0
func loadUser() async throws {
loadUserCallCount += 1
}
}
}A: このパターンはDependencyProtocolへの移行パスが確保されています:
// 現在のパターン(軽量版)
let mock = Mocks.UserRepository()
let store = UserStore(repository: mock)
// 将来的にDependencyProtocolパターンへ移行
enum MockDependency: DependencyProtocol {
typealias UserRepository = Mocks.UserRepository // ← 既存のモックを再利用
}
let store = UserStore<MockDependency>(repository: .init())モック実装自体は変更不要で、型システム側だけ変更すれば移行できます。
この軽量版アプローチにより:
- ✅ モック定義の重複を即座に解消
- ✅ 既存コードへの影響を最小化
- ✅ チーム全体でモック管理の一貫性を確保
- ✅ 将来的な高度なパターンへの移行も可能
まずは1つのRepositoryから始めて、効果を確認しながら段階的に展開していくことをお勧めします。