Files
John Qiu 712063071c refactor: 通用技能按类别拆分为独立目录
skills/ → skills-dev(9), skills-req(10), skills-ops(4),
skills-integration(8), skills-biz(4), skills-workflow(7)

generate-marketplace.py 改为自动扫描所有 skills-* 目录。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 11:31:58 +10:30

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 |