158 lines
4.1 KiB
Markdown
158 lines
4.1 KiB
Markdown
# 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<TaskListResponse, Error> = .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<Method>_<Scenario>` 格式
|
|
|
|
## Xcode 快捷键
|
|
|
|
| 快捷键 | 操作 |
|
|
|--------|------|
|
|
| `Cmd + U` | 运行所有测试 |
|
|
| `Ctrl + Opt + Cmd + U` | 运行当前测试方法 |
|
|
| `Ctrl + Opt + Cmd + G` | 重新运行上次测试 |
|
|
| `Cmd + 6` | Test Navigator |
|