如何对使用单例模式的模型控制器及UIViewController进行单元测试
前置说明
你提供的代码存在两处可提前修复的编译/逻辑问题,修复后更便于后续测试:
Entry为值类型且title、body声明为let常量,现有updateEntry方法修改的是传入的Entry副本,既会触发编译报错,也无法同步修改到entries数组中Entry未实现Equatable协议,destroyEntry方法中调用index(of:)会触发编译报错
问题1:如何测试单例CRUD函数,以及单元测试覆盖范围
- 单例测试的核心痛点是全局状态会在多个测试用例之间互相污染,解决方案如下:
- 先定义抽象协议,解耦业务代码对单例的直接依赖:
protocol EntryControllerProtocol { var entries: [Entry] { get set } func newEntry(title: String, body: String) func updateEntry(title: String, body: String, entry: Entry) func destroyEntry(entry: Entry) } // 让原有EntryController实现该协议 extension EntryController: EntryControllerProtocol {}
- 测试时不要使用
EntryController.shared,每次测试都初始化独立的EntryController实例,从根源避免状态互相影响 - CRUD基础测试用例:
- 新增:调用newEntry前entries为空,调用后数组长度为1,元素的title、body与入参一致
- 更新:先插入测试Entry,调用update后验证数组中对应位置的Entry属性符合预期(需先修复原update方法的bug)
- 删除:先插入测试Entry,调用destroyEntry后entries为空;删除不存在的Entry时无崩溃、无异常
- 单元测试覆盖范围不局限于单个函数,还要覆盖对应单元的所有行为边界:比如空数据处理、非法入参处理、重复数据插入/删除的表现等,只要是EntryController对外承诺的行为都属于测试范围。
问题2:CRUD改为静态函数后的测试方法
静态函数本质是操作全局静态存储的entries数组,测试核心是每次测试前重置全局状态:
- 保持抽象协议的设计,静态方法同样可以在协议中声明
- 在测试用例的
setUp方法中,每次执行测试前都将静态的entries数组重置为空数组,避免上一个测试的残留数据影响下一个测试的结果 - 测试用例逻辑和普通实例方法的测试完全一致,仅调用方从实例改为类本身即可
注:静态方法的全局状态特性天生不便于测试,非必要场景更推荐使用依赖注入的实例方案
问题3:UIViewController的单元测试方法
核心思路是对控制器的外部依赖做注入,避免依赖全局单例,同时单独测试控制器的业务逻辑:
- 先改造EntryViewController,把对
EntryController.shared的硬编码改为可注入的依赖:
class EntryViewController: TableViewController { // 依赖注入,默认值为生产环境的单例 var entryController: EntryControllerProtocol = EntryController.shared // 原有其他代码保持不变,把所有用到EntryController.shared的地方替换为entryController }
- 测试数据源方法:
- 测试
numberOfRowsInSection:给注入的entryController传入指定数量的测试Entry,验证方法返回值和Entry数量一致 - 测试
cellForRowAt:提前给测试用的tableView注册对应identifier的UITableViewCell,调用方法后验证cell的textLabel、detailTextLabel内容与对应位置的Entry属性一致 - 空数据测试:entryController的entries为空时,验证数据源方法返回0,不会出现数组越界崩溃
- 如果需要测试交互逻辑(比如左滑删除、点击跳转),可以通过调用对应代理方法,验证依赖的entryController是否执行了对应操作即可。
内容的提问来源于stack exchange,提问作者Timothy
相关产品推荐
相关产品推荐

