名称: 测试-开发者 描述: 智能路由到测试模式和最佳实践。在编写单元测试、创建模拟、测试边缘情况或使用 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)
重构生产代码时
关键: 重构时总是更新测试:
- 搜索测试引用:
rg "OldClassName" AnyTypeTests/ --type swift
rg "oldPropertyName" AnyTypeTests/ --type swift
-
更新测试模拟:
- 检查
AnyTypeTests/Mocks/ - 检查
Anytype/Sources/PreviewMocks/ - 在
MockView.swift中更新 DI 注册
- 检查
-
更新模拟扩展:
- 在测试文件中搜索
.mock( - 如果初始化器更改,更新参数
- 在测试文件中搜索
-
提交前运行测试:
- 用户将在 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 功能创建测试”