第 10 章:测试 SwiftData 应用
单元测试:使用内存存储进行快速测试
为什么需要单元测试 SwiftData 应用?
- 验证数据模型的正确性(属性、关系、业务逻辑)
- 确保 CRUD 操作按预期工作
- 提前发现数据迁移问题
- 提高代码可维护性和重构安全性
配置测试环境
import XCTest
import SwiftData
@testable import YourAppName
final class YourDataModelTests: XCTestCase {
var modelContainer: ModelContainer!
var modelContext: ModelContext!
override func setUp() {
super.setUp()
// 配置内存中的临时存储
let config = ModelConfiguration(isStoredInMemoryOnly: true)
modelContainer = try! ModelContainer(for: YourModel.self, configurations: config)
modelContext = ModelContext(modelContainer)
}
override func tearDown() {
modelContext = nil
modelContainer = nil
super.tearDown()
}
}
核心测试场景
1. 测试模型创建与保存
func testCreateAndSaveItem() throws {
let testItem = Item(name: "Test", timestamp: Date())
modelContext.insert(testItem)
try modelContext.save()
let fetchDescriptor = FetchDescriptor<Item>()
let items = try modelContext.fetch(fetchDescriptor)
XCTAssertEqual(items.count, 1)
XCTAssertEqual(items.first?.name, "Test")
}
2. 测试删除操作
func testDeleteItem() throws {
let testItem = Item(name: "ToDelete", timestamp: Date())
modelContext.insert(testItem)
try modelContext.save()
modelContext.delete(testItem)
try modelContext.save()
let fetchDescriptor = FetchDescriptor<Item>()
let items = try modelContext.fetch(fetchDescriptor)
XCTAssertTrue(items.isEmpty)
}
3. 测试关系操作
func testOneToManyRelationship() throws {
let parent = Parent(name: "Parent")
let child = Child(name: "Child")
parent.children.append(child)
modelContext.insert(parent)
try modelContext.save()
let fetchedParent = try modelContext.fetch(FetchDescriptor<Parent>()).first!
XCTAssertEqual(fetchedParent.children.count, 1)
XCTAssertEqual(fetchedParent.children.first?.name, "Child")
}
高级测试技巧
使用 Predicate 测试查询
func testPredicateQuery() throws {
let item1 = Item(name: "Important", timestamp: Date())
let item2 = Item(name: "Trivial", timestamp: Date())
modelContext.insert(item1)
modelContext.insert(item2)
try modelContext.save()
let predicate = #Predicate<Item> { $0.name == "Important" }
let descriptor = FetchDescriptor<Item>(predicate: predicate)
let results = try modelContext.fetch(descriptor)
XCTAssertEqual(results.count, 1)
XCTAssertEqual(results.first?.name, "Important")
}
测试自定义模型方法
// 假设 Item 模型有一个计算属性
extension Item {
var isImportant: Bool { name.contains("Important") }
}
func testCustomModelLogic() throws {
let item = Item(name: "Important Task", timestamp: Date())
XCTAssertTrue(item.isImportant)
}
测试最佳实践
- 隔离性:每个测试应该独立运行,不依赖其他测试的状态
- 速度优化:使用内存存储(
isStoredInMemoryOnly: true)避免磁盘 I/O - 确定性:避免使用随机数据,确保测试可重复
- 清理资源:在
tearDown中释放所有测试资源 - 边界测试:特别测试空值、极值等边界情况
常见问题解决方案
问题:测试间数据污染
// 解决方案:在每个测试后清空上下文
override func tearDown() {
let items = try? modelContext.fetch(FetchDescriptor<Item>())
items?.forEach { modelContext.delete($0) }
try? modelContext.save()
super.tearDown()
}
问题:测试异步操作
func testAsyncOperation() {
let expectation = XCTestExpectation(description: "Async save completion")
DispatchQueue.global().async {
let newItem = Item(name: "Async", timestamp: Date())
self.modelContext.insert(newItem)
try! self.modelContext.save()
expectation.fulfill()
}
wait(for: [expectation], timeout: 1.0)
}
