Files
ai-proj-helper/plugins/dev-test-plugin/skills/dev-test/ios-testing.md

4.1 KiB

iOS 测试 (XCTest + Swift Concurrency)

测试框架

  • XCTest: Apple 官方测试框架
  • Swift Testing: Swift 6 新测试框架 (可选)
  • ViewInspector: SwiftUI 视图测试 (第三方)

运行测试

# 全部测试
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 注入

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

@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() 方法

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

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

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