# iOS 测试 (XCTest + Swift Concurrency) ## 测试框架 - **XCTest**: Apple 官方测试框架 - **Swift Testing**: Swift 6 新测试框架 (可选) - **ViewInspector**: SwiftUI 视图测试 (第三方) ## 运行测试 ```bash # 全部测试 xcodebuild test \ -scheme AI-Proj-iOS \ -destination 'platform=iOS Simulator,name=iPhone 16' \ -quiet # 特定测试类 xcodebuild test \ -scheme AI-Proj-iOS \ -destination 'platform=iOS Simulator,name=iPhone 16' \ -only-testing:AI-Proj-iOSTests/DashboardViewModelTests # 覆盖率 xcodebuild test \ -scheme AI-Proj-iOS \ -destination 'platform=iOS Simulator,name=iPhone 16' \ -enableCodeCoverage YES ``` ## 项目测试结构 (AI-Proj-iOS) ``` AI-Proj-iOSTests/ ├── Mocks/ │ ├── MockServices.swift # Mock 服务协议实现 │ └── MockNetworkService.swift ├── ViewModels/ │ ├── DashboardViewModelTests.swift │ ├── TaskViewModelTests.swift │ └── RequirementViewModelTests.swift ├── Services/ │ ├── TaskServiceTests.swift │ └── DashboardAggregationServiceTests.swift ├── Models/ │ └── ModelDecodingTests.swift └── Utilities/ └── DateFormatterTests.swift ``` ## 关键模式 ### 1. Mock 服务 — Result 注入 ```swift class MockTaskService: TaskServiceProtocol { var fetchTasksResult: Result = .success(.mock) func fetchTasks(...) async throws -> TaskListResponse { switch fetchTasksResult { case .success(let response): return response case .failure(let error): throw error } } } ``` 所有 Mock 服务统一用 `Result` 属性控制成功/失败返回。 ### 2. ViewModel 测试 — @MainActor + async ```swift @MainActor final class DashboardViewModelTests: XCTestCase { var sut: DashboardViewModel! var mockService: MockDashboardAggregationService! override func setUp() { super.setUp() mockService = MockDashboardAggregationService() sut = DashboardViewModel(dashboardService: mockService) } override func tearDown() { sut = nil; mockService = nil super.tearDown() } func testLoadDashboardData_Success() async { mockService.fetchDashboardDataResult = .success(expectedData) await sut.loadDashboardData() XCTAssertFalse(sut.isLoading) XCTAssertEqual(sut.todayStats.completedTasks, 5) } } ``` 要点:`@MainActor` + `async` 测试方法 + setUp/tearDown 重置。 ### 3. Mock 数据工厂 — 静态 `.mock()` 方法 ```swift extension TaskModel { static func mock(id: Int = 1, status: TaskStatus = .todo) -> TaskModel { TaskModel(id: id, title: "Mock Task", status: status, ...) } } extension TaskListResponse { static var mock: TaskListResponse { TaskListResponse(tasks: [.mock(id: 1), .mock(id: 2)], total: 2, page: 1, pageSize: 20) } } ``` ### 4. 模型解码测试 — JSON → Model ```swift func testTaskModel_DecodesFromJSON() throws { let json = """ { "id": 123, "status": "in_progress", "priority": "high", ... } """.data(using: .utf8)! let task = try decoder.decode(TaskModel.self, from: json) XCTAssertEqual(task.status, .inProgress) } ``` ### 5. SwiftUI 视图测试 — ViewInspector ```swift extension EnhancedStatsSection: Inspectable {} func testStatsSection_DisplaysCorrectValues() throws { let view = EnhancedStatsSection(stats: .mock) let text = try view.inspect().find(text: "5") XCTAssertNotNil(text) } ``` ## 最佳实践 1. **@MainActor** — ViewModel 测试必须在主线程 2. **Mock 所有依赖** — 协议抽象 + Result 注入 3. **async/await** — 避免 XCTestExpectation 回调 4. **数据工厂** — `.mock()` 静态方法,参数带默认值 5. **隔离测试** — setUp/tearDown 重置所有状态 6. **命名** — `test_` 格式 ## Xcode 快捷键 | 快捷键 | 操作 | |--------|------| | `Cmd + U` | 运行所有测试 | | `Ctrl + Opt + Cmd + U` | 运行当前测试方法 | | `Ctrl + Opt + Cmd + G` | 重新运行上次测试 | | `Cmd + 6` | Test Navigator |