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>
This commit is contained in:
157
skills-dev/dev-test-plugin/skills/dev-test/ios-testing.md
Normal file
157
skills-dev/dev-test-plugin/skills/dev-test/ios-testing.md
Normal file
@@ -0,0 +1,157 @@
|
||||
# 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 |
|
||||
Reference in New Issue
Block a user