第7章:测试 MVVM 应用
单元测试 Model 和 ViewModel
1. 单元测试的重要性
在 MVVM 架构中,Model 和 ViewModel 是业务逻辑的核心承载层。通过单元测试可以:
- 验证数据模型的正确性(如属性、计算逻辑)
- 确保 ViewModel 的行为符合预期(如状态更新、事件响应)
- 提高代码可维护性,减少回归错误
2. 测试 Model 层
测试要点:
struct TodoItem: Identifiable {
let id: UUID
var title: String
var isCompleted: Bool
// 测试计算属性示例
var statusDescription: String {
isCompleted ? "已完成" : "未完成"
}
}
// 测试用例
class TodoModelTests: XCTestCase {
func testStatusDescription() {
let item1 = TodoItem(id: UUID(), title: "任务1", isCompleted: true)
XCTAssertEqual(item1.statusDescription, "已完成")
let item2 = TodoItem(id: UUID(), title: "任务2", isCompleted: false)
XCTAssertEqual(item2.statusDescription, "未完成")
}
}
测试策略:
- 验证数据初始状态
- 测试自定义方法/计算属性
- 边界条件测试(如空值、极端值)
3. 测试 ViewModel 层
测试环境搭建:
class TodoListViewModel: ObservableObject {
@Published var items: [TodoItem] = []
func addItem(title: String) {
let newItem = TodoItem(id: UUID(), title: title, isCompleted: false)
items.append(newItem)
}
func toggleCompletion(for item: TodoItem) {
if let index = items.firstIndex(where: { $0.id == item.id }) {
items[index].isCompleted.toggle()
}
}
}
class TodoViewModelTests: XCTestCase {
var viewModel: TodoListViewModel!
override func setUp() {
super.setUp()
viewModel = TodoListViewModel()
}
}
核心测试场景:
// 测试添加功能
func testAddItem() {
let initialCount = viewModel.items.count
viewModel.addItem(title: "测试任务")
XCTAssertEqual(viewModel.items.count, initialCount + 1)
XCTAssertEqual(viewModel.items.last?.title, "测试任务")
}
// 测试状态切换
func testToggleCompletion() {
viewModel.addItem(title: "测试任务")
let item = viewModel.items[0]
// 初始状态验证
XCTAssertFalse(item.isCompleted)
// 第一次切换
viewModel.toggleCompletion(for: item)
XCTAssertTrue(viewModel.items[0].isCompleted)
// 第二次切换
viewModel.toggleCompletion(for: viewModel.items[0])
XCTAssertFalse(viewModel.items[0].isCompleted)
}
4. 高级测试技巧
异步操作测试:
func testAsyncDataLoading() {
let expectation = XCTestExpectation(description: "Data loaded")
// 模拟网络请求
DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
self.viewModel.addItem(title: "异步任务")
expectation.fulfill()
}
wait(for: [expectation], timeout: 2)
XCTAssertFalse(viewModel.items.isEmpty)
}
依赖注入测试:
protocol DataService {
func fetchTodos() -> [TodoItem]
}
class MockDataService: DataService {
func fetchTodos() -> [TodoItem] {
return [TodoItem(id: UUID(), title: "Mock任务", isCompleted: true)]
}
}
class ViewModelWithDependency {
let service: DataService
@Published var items: [TodoItem] = []
init(service: DataService) {
self.service = service
}
func loadData() {
items = service.fetchTodos()
}
}
func testDependencyInjection() {
let mockService = MockDataService()
let vm = ViewModelWithDependency(service: mockService)
vm.loadData()
XCTAssertEqual(vm.items.count, 1)
XCTAssertEqual(vm.items[0].title, "Mock任务")
}
5. 测试最佳实践
- 命名规范:使用
test[被测单元]_[条件]_[预期结果]格式 - 测试隔离:每个测试方法应该是独立的
- 断言明确:避免通用断言,应验证具体值
- 测试覆盖率:优先覆盖核心业务逻辑
- CI 集成:将测试纳入持续集成流程
6. 常见问题解决方案
| 问题类型 | 解决方案 |
|---|---|
| 测试随机失败 | 使用固定种子或模拟随机数生成 |
| 异步测试超时 | 合理设置 timeout 值 |
| 状态污染 | 在 setUp/tearDown 中重置状态 |
| 复杂依赖 | 使用协议抽象依赖 |
通过系统化的单元测试,可以显著提升 MVVM 应用的可靠性和开发效率。建议结合 Xcode 的 Test Navigator 和 Code Coverage 工具持续优化测试质量。
