如何从Storyboard创建Mock视图控制器解决IBOutlet空值报错
如何正确从Storyboard加载Mock视图控制器避免IBOutlet空值报错
问题描述
需要为Storyboard创建的视图控制器构建对应的Mock视图控制器,尝试以下方案均存在问题:
- 直接向下转型Storyboard实例化的对象
Storyboard加载的视图控制器通过编码初始化,向下转型到子类MockViewController失败,示例代码:let _storyboard = UIStoryboard(name: "Main", bundle: nil) let vc = _storyboard.instantiateViewController(ofType: ViewController.self) as? MockViewController - 通过nib名称初始化Mock视图控制器
初始化后@IBOutlet属性为空,触发运行时错误,示例代码:class MockViewController: ViewController { init() { let controllerName = String(describing: ViewController.self) super.init(nibName: controllerName, bundle: nil) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } } - 手动同步属性
先加载原始ViewController实例,再创建Mock实例后逐个给属性赋值。该方案效率极低,生产代码变更时必须同步修改测试代码,维护成本极高,示例代码:let _storyboard = UIStoryboard(name: "Main", bundle: nil) let vc = _storyboard.instantiateViewController(ofType: ViewController.self) let mockViewController = MockViewController() mockViewController.someProperty = vc.property - 使用带creator闭包的实例化方法
在闭包中用传入的coder初始化MockViewController,实现init(coder:)并调用父类初始化方法,仍然出现@IBOutlet属性为空的错误,示例代码:let controllerName = String(describing: ViewController.self) let mockVC = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(identifier: controllerName, creator: { coder -> MockViewController? in return MockViewController(coder: coder) })required init?(coder: NSCoder) { super.init(coder: coder) }
可行方案
IBOutlet连线失败的本质是:Storyboard序列化时存储的是Interface Builder中绑定的原始类信息,解码时会按照存储的类完成属性连线,直接返回子类实例的写法会破坏默认的连线流程。以下两种方案可以解决问题,且维护成本极低:
方案1:运行时临时替换类映射(无需修改资源文件)
不需要使用creator闭包,在实例化控制器前,通过Runtime临时将原始类替换为Mock类,让Storyboard解码时直接创建Mock实例,连线完成后恢复原始类即可:
// 保存原始类引用 let originalClass = ViewController.self let mockClass = MockViewController.self // 临时替换类映射 object_setClass(originalClass, mockClass) let storyboard = UIStoryboard(name: "Main", bundle: nil) // 正常调用实例化方法,得到的实际是Mock实例 guard let mockVC = storyboard.instantiateViewController(ofType: ViewController.self) as? MockViewController else { fatalError("Mock实例化失败") } // 恢复原始类映射,避免影响其他逻辑 object_setClass(originalClass, originalClass)
Mock子类不需要重写初始化方法,只重写需要Mock的业务方法即可,父类的初始化、IBOutlet连线逻辑会正常执行,不会出现空值问题。
单元测试场景下,建议在
setUp()方法中执行类替换,tearDown()方法中恢复原始类,避免类替换污染其他测试用例。
方案2:测试Target独立Storyboard(零Runtime代码)
在测试Target中复制一份原Storyboard文件,将对应控制器的Class修改为MockViewController,保持Storyboard ID与原Storyboard一致。测试时加载测试Bundle下的Storyboard即可,不需要写任何特殊初始化逻辑,所有IBOutlet会自动完成连线,生产代码完全不需要修改,后续UI变动时只需要同步更新测试Target下的Storyboard即可。
内容的提问来源于stack exchange,提问作者Kevvv
相关产品推荐
相关产品推荐

