测试开发技能Skill tests-developer

这个技能是一个智能路由器,专注于测试模式和最佳实践,帮助开发者编写单元测试、创建模拟、测试边缘情况等,适用于 Swift Testing 和 XCTest 框架。关键词包括测试开发、单元测试、Swift Testing、XCTest、模拟测试、边缘测试。

测试 0 次安装 0 次浏览 更新于 3/15/2026

名称: 测试-开发者 描述: 智能路由到测试模式和最佳实践。在编写单元测试、创建模拟、测试边缘情况或使用 Swift Testing 和 XCTest 框架时使用。

测试开发者技能

智能路由到测试模式和最佳实践。编写单元测试、创建模拟、测试组织。

何时使用此技能

此技能在以下情况下激活:

  • 编写单元测试
  • 创建测试模拟
  • 测试边缘情况
  • 测试驱动开发 (TDD)
  • 测试重构和更新
  • Swift Testing 框架使用
  • XCTest 框架使用

快速参考

关键规则

  • 使用 Swift Testing 框架 (import Testing, @Test, @Suite) 用于新测试
  • 保留现有 XCTest 测试 保持不变(除非必要,否则不要迁移)
  • 测试边缘情况:空值、空集合、边界条件
  • 在测试文件扩展中创建模拟助手 当需要时
  • 重构时更新测试 - 总是搜索并更新引用
  • 永远不要跳过测试 对于数据转换或业务逻辑
  • 永远不要在测试中使用强制解包 - 使用适当的断言

测试文件命名

生产代码:  SetContentViewDataBuilder.swift
测试文件:       SetContentViewDataBuilderTests.swift
位置:        任何类型测试/[类别]/[测试文件].swift

Swift Testing 框架(新测试的首选)

基本结构:

import Testing
import Foundation
@testable import Anytype
import Services

@Suite
struct MyFeatureTests {

    private let sut: MyFeature  // 测试对象

    init() {
        self.sut = MyFeature()
    }

    @Test func testSpecificBehavior() {
        // 安排
        let input = "test"

        // 行动
        let result = sut.process(input)

        // 断言
        #expect(result == "expected")
    }

    @Test func testEdgeCase_EmptyInput_ReturnsNil() {
        let result = sut.process("")
        #expect(result == nil)
    }
}

套件选项:

@Suite                      // 并行执行(默认)
@Suite(.serialized)        // 顺序执行(用于共享状态)

断言:

#expect(value == expected)           // 相等性
#expect(value != unexpected)         // 不相等性
#expect(result != nil)               // 非空
#expect(array.isEmpty)               // 布尔条件
#expect(throws: SomeError.self) {    // 错误抛出
    try throwingFunction()
}

XCTest 框架(遗留测试)

基本结构:

import XCTest
@testable import Anytype

final class MyFeatureTests: XCTestCase {

    var sut: MyFeature!

    override func setUpWithError() throws {
        sut = MyFeature()
    }

    override func tearDownWithError() throws {
        sut = nil
    }

    func testSpecificBehavior() {
        // 安排
        let input = "test"

        // 行动
        let result = sut.process(input)

        // 断言
        XCTAssertEqual(result, "expected")
    }
}

常见断言:

XCTAssertEqual(actual, expected)
XCTAssertNotEqual(actual, unexpected)
XCTAssertNil(value)
XCTAssertNotNil(value)
XCTAssertTrue(condition)
XCTAssertFalse(condition)
XCTAssertThrowsError(try expression)

测试组织模式

1. 边缘情况测试(关键)

总是测试这些场景:

@Test func testEmptyInput() {
    let result = sut.process([])
    #expect(result.isEmpty)
}

@Test func testNilInput() {
    let result = sut.process(nil)
    #expect(result == nil)
}

@Test func testSingleItem() {
    let result = sut.process([item])
    #expect(result.count == 1)
}

@Test func testBoundaryCondition() {
    let items = (0..<100).map { Item(id: "\($0)") }
    let result = sut.process(items)
    #expect(result.count <= 100)
}

@Test func testTruncation_LimitsToMax() {
    let attachments = (0..<5).map { ObjectDetails.mock(id: "item\($0)") }
    let result = sut.truncate(attachments, limit: 3)
    #expect(result.count == 3)
    #expect(result[0].id == "item0")
    #expect(result[2].id == "item2")
}

2. 模拟助手(在测试文件中)

位置: 在同一测试文件中创建扩展

// 在测试文件底部
extension ObjectDetails {
    static func mock(id: String) -> ObjectDetails {
        ObjectDetails(id: id, values: [:])
    }
}

extension Participant {
    static func mock(
        id: String,
        globalName: String = "",
        icon: ObjectIcon? = nil
    ) -> Participant {
        Participant(
            id: id,
            localName: "",
            globalName: globalName,
            icon: icon,
            status: .active,
            permission: .reader,
            identity: "",
            identityProfileLink: "",
            spaceId: "",
            type: ""
        )
    }
}

3. 测试中的依赖注入

使用工厂模式:

@Suite(.serialized)  // DI 设置所需
struct MyFeatureTests {

    private let mockService: MyServiceMock

    init() {
        let mockService = MyServiceMock()
        Container.shared.myService.register { mockService }
        self.mockService = mockService
    }

    @Test func testWithMockedDependency() {
        mockService.expectedResult = "test"
        let sut = MyFeature()
        let result = sut.doWork()
        #expect(result == "test")
    }
}

4. 测试协议(将方法设为内部)

问题: 私有方法无法测试 解决方案: 使用 internal 访问权限并通过协议测试

// 生产代码 - SetContentViewDataBuilder.swift
final class SetContentViewDataBuilder: SetContentViewDataBuilderProtocol {

    // ✅ 内部用于测试(非私有)
    func buildChatPreview(
        objectId: String,
        spaceView: SpaceView?,
        chatPreviewsDict: [String: ChatMessagePreview]
    ) -> MessagePreviewModel? {
        // 实现
    }
}

// 测试代码
@Test func testBuildChatPreview_EmptyDict_ReturnsNil() {
    let result = builder.buildChatPreview(
        objectId: "test",
        spaceView: nil,
        chatPreviewsDict: [:]
    )
    #expect(result == nil)
}

5. 字典转换测试

性能验证:

@Test func testDictionaryConversion_EmptyArray() {
    let items: [Item] = []
    let dict = Dictionary(uniqueKeysWithValues: items.map { ($0.id, $0) })
    #expect(dict.isEmpty)
}

@Test func testDictionaryConversion_MultipleItems() {
    let items = (0..<10).map { Item(id: "item\($0)") }
    let dict = Dictionary(uniqueKeysWithValues: items.map { ($0.id, $0) })

    #expect(dict.count == 10)
    for i in 0..<10 {
        #expect(dict["item\(i)"] != nil)
    }
}

@Test func testDictionaryLookup_O1Performance() {
    let items = (0..<100).map { Item(id: "item\($0)") }
    let dict = Dictionary(uniqueKeysWithValues: items.map { ($0.id, $0) })

    let result = dict["item50"]
    #expect(result != nil)
    #expect(result?.id == "item50")
}

6. 测试日期

@Test func testDateFormatting() {
    let date = Date(timeIntervalSince1970: 1700000000)
    let result = formatter.format(date)
    #expect(result.isEmpty == false)
}

@Test func testDateComparison() {
    let now = Date()
    let future = now.addingTimeInterval(3600)
    #expect(future > now)
}

7. 测试 Protobuf 模型

ChatState 示例:

@Test func testChatStateCounters() {
    var chatState = ChatState()

    var messagesState = ChatState.UnreadState()
    messagesState.counter = 5
    chatState.messages = messagesState

    var mentionsState = ChatState.UnreadState()
    mentionsState.counter = 2
    chatState.mentions = mentionsState

    #expect(chatState.messages.counter == 5)
    #expect(chatState.mentions.counter == 2)
}

模拟服务模式

预览模拟(用于 SwiftUI 预览)

位置: Anytype/Sources/PreviewMocks/

用法:

import SwiftUI

#Preview {
    MockView {
        // 配置模拟状态
        SpaceViewsStorageMock.shared.workspaces = [...]
    } content: {
        MyView()
    }
}

测试模拟(用于单元测试)

位置: AnyTypeTests/Mocks/ 或在测试文件中

模式:

@testable import Anytype

final class MyServiceMock: MyServiceProtocol {
    var callCount = 0
    var capturedInput: String?
    var stubbedResult: Result?

    func process(_ input: String) -> Result {
        callCount += 1
        capturedInput = input
        return stubbedResult ?? .default
    }
}

测试检查清单

编写测试时,确保:

  • [ ] 测试正常路径(有效输入,预期输出)
  • [ ] 测试边缘情况(空、空值、边界条件)
  • [ ] 测试错误条件(无效输入,抛出函数)
  • [ ] 测试数据转换(截断、过滤、映射)
  • [ ] 测试性能假设(O(1) 查找、O(n) 操作)
  • [ ] 为复杂类型创建模拟助手
  • [ ] 使用描述性测试名称:testFeature_Condition_ExpectedBehavior
  • [ ] 遵循 AAA 模式:安排、行动、断言
  • [ ] 没有强制解包(!) - 使用适当的断言
  • [ ] 只导入必要的模块(@testable import Anytype

重构生产代码时

关键: 重构时总是更新测试:

  1. 搜索测试引用:
rg "OldClassName" AnyTypeTests/ --type swift
rg "oldPropertyName" AnyTypeTests/ --type swift
  1. 更新测试模拟:

    • 检查 AnyTypeTests/Mocks/
    • 检查 Anytype/Sources/PreviewMocks/
    • MockView.swift 中更新 DI 注册
  2. 更新模拟扩展:

    • 在测试文件中搜索 .mock(
    • 如果初始化器更改,更新参数
  3. 提交前运行测试:

    • 用户将在 Xcode 中验证(缓存更快)
    • 向用户报告所有测试文件更改

代码库中的常见测试模式

模式 1: 构建器测试

@Test func testBuilderCreatesCorrectModel() {
    let input = [...setup...]
    let result = builder.build(input)

    #expect(result.property1 == expected1)
    #expect(result.property2 == expected2)
    #expect(result.collection.count == 3)
}

模式 2: 存储/仓库测试

@Suite(.serialized)
struct StorageTests {
    init() {
        // 设置模拟依赖
    }

    @Test func testSaveAndRetrieve() {
        storage.save(item)
        let retrieved = storage.get(item.id)
        #expect(retrieved?.id == item.id)
    }
}

模式 3: 解析器/格式化程序测试

@Test func testParseValidInput() {
    let result = parser.parse("valid input")
    #expect(result != nil)
}

@Test func testParseInvalidInput_ReturnsNil() {
    let result = parser.parse("")
    #expect(result == nil)
}

模式 4: 计数器/状态测试

@Test func testCountersPropagation() {
    var model = Model()
    model.state = createState(messages: 5, mentions: 2)

    #expect(model.unreadCounter == 5)
    #expect(model.mentionCounter == 2)
}

测试文件示例

示例 1: SetContentViewDataBuilderTests.swift

完整工作示例: AnyTypeTests/Services/SetContentViewDataBuilderTests.swift

  • 测试构建器方法
  • 创建模拟助手
  • 测试边缘情况(空、空值、截断)
  • 测试计数器传播
  • 测试字典转换性能

示例 2: ChatMessageLimitsTests.swift

完整工作示例: AnyTypeTests/Services/ChatMessageLimitsTests.swift

  • 使用 @Suite(.serialized) 进行 DI 设置
  • 通过工厂 DI 模拟日期提供者
  • 测试速率限制逻辑
  • 测试基于时间的条件

相关文档

  • CLAUDE.md: 项目指南,无评论规则,测试要求
  • IOS_DEVELOPMENT_GUIDE.md: Swift 模式,MVVM 架构
  • .claude/CODE_REVIEW_GUIDE.md: 审查标准包括测试更新

导航: 这是一个智能路由器。有关全面的测试指南和架构模式,请参阅 IOS_DEVELOPMENT_GUIDE.md

快速帮助: 只需询问“如何测试 X?”或“为 Y 功能创建测试”