手动实例化UIView到Mock UIViewController后为何仍为nil?
解决XCUnitTest中Mock UIViewController时手动实例化UIView/UIStackView仍为nil的问题
我之前在做UIViewController的单元测试时也碰到过一模一样的问题!明明手动给Mock子类的UIView/UIStackView属性赋了值,但运行测试时却发现它还是nil,折腾了好一会儿才找到原因,分享几个我亲测有效的解决思路:
原因分析
最常见的两个诱因是:
- 弱引用被释放:IBOutlet默认是
weak修饰的,如果你手动创建的UIView/UIStackView只赋值给这个weak属性,没有其他强引用持有它,ARC会直接把它回收,导致属性变回nil——毕竟UIKit加载xib/storyboard时,会把视图添加到视图层级里形成强引用,但手动创建的视图没有这个环境。 - 视图懒加载未触发:UIViewController的
view是懒加载的,如果你的测试逻辑里没触发viewDidLoad(比如没访问过vc.view),可能某些和视图相关的初始化逻辑没走,或者属性赋值的时机不对。
具体解决方案
1. 处理弱引用问题
有两种方式:
- 修改Mock子类的属性修饰符:把原ViewController里的
weak var stackView: UIStackView!改成var stackView: UIStackView!(去掉weak),这样手动赋值的视图会被强引用持有,不会被释放。示例代码:class MockMyVC: MyViewController { // 去掉weak,改为强引用 var stackView: UIStackView! // 其他Mock逻辑,比如记录performSegue的调用次数 var didCallPerformSegue = false override func performSegue(withIdentifier identifier: String, sender: Any?) { didCallPerformSegue = true } } - 在测试代码中强持有视图:如果不想修改原属性的修饰符,可以在测试里创建一个强引用的变量来持有手动实例化的视图:
func testViewDidLoadLogic() { let mockVC = MockMyVC() // 强持有测试用的stackView let testStackView = UIStackView() mockVC.stackView = testStackView // 触发view加载 _ = mockVC.view // 验证你的业务逻辑 XCTAssertTrue(...) }
2. 确保触发视图加载
UIViewController的viewDidLoad只有在第一次访问view属性时才会被调用,所以如果你的测试逻辑依赖viewDidLoad里的代码,一定要先确保视图已经加载。最直接的方式就是在测试里主动访问vc.view:
func testSomethingInViewDidLoad() { let mockVC = MockMyVC() // 先设置好所有需要Mock的属性 mockVC.stackView = UIStackView() // 触发viewDidLoad _ = mockVC.view // 这里再进行断言验证 }
3. 重构原VC,用依赖注入优化测试
如果经常碰到这类测试问题,推荐重构原ViewController,让它的子视图可以通过初始化参数传入,彻底摆脱对IBOutlet的依赖,测试会更顺畅:
// 原ViewController重构后 class MyViewController: UIViewController { let stackView: UIStackView // 供测试使用的初始化方法 init(stackView: UIStackView) { self.stackView = stackView super.init(nibName: nil, bundle: nil) } // 适配xib/storyboard的初始化方法 required init?(coder: NSCoder) { self.stackView = UIStackView() super.init(coder: coder) } override func viewDidLoad() { super.viewDidLoad() // 正常使用stackView的业务逻辑 stackView.addArrangedSubview(...) } }
这样测试时直接传入自己创建的stackView,完全不需要Mock子类,也不会有nil的问题:
func testViewDidLoad() { let testStackView = UIStackView() let vc = MyViewController(stackView: testStackView) _ = vc.view // 验证stackView是否被正确配置 XCTAssertEqual(testStackView.arrangedSubviews.count, 1) }
调试小技巧
如果还是不确定问题出在哪,可以在Mock子类里给属性加didSet监听,看看有没有被正确赋值:
class MockMyVC: MyViewController { override var stackView: UIStackView! { didSet { print("stackView被赋值为: \(stackView)") } } }
运行测试时看控制台输出,就能知道赋值操作有没有生效,以及后续是不是被置为nil了。
内容的提问来源于stack exchange,提问作者ramnik12358
相关产品推荐
相关产品推荐

