iOS视图状态变更单元测试遇nil崩溃,求正确测试方法
这个问题我太熟悉了!你遇到的Thread 1: Fatal error崩溃,根本原因是直接用SampleViewController()初始化实例时,它的IBOutlet并没有被正确绑定——因为IBOutlet是从XIB/Storyboard的界面文件中解析加载的,单纯通过代码初始化ViewController,sampleView会一直是nil,调用hideView()/showView()时自然会触发解包可选值的崩溃。
同时,你原来的断言逻辑也有问题:你要测试的是视图的isHidden状态变化,而不是视图本身是否存在,所以XCTAssertNil/XCTAssertNotNil完全不符合测试目标。
下面给你两种可行的解决方案,从基础修复到优雅优化:
方案一:修复视图加载逻辑,正确测试状态
如果你还是想依赖XIB/Storyboard来测试,那必须确保ViewController的视图被加载,这样IBOutlet才能被绑定。
第一步:修正ViewController的初始化方式
在测试的setUp方法中,通过Storyboard或XIB初始化ViewController,并手动触发视图加载:
override func setUp() { super.setUp() // 如果你用的是Storyboard let storyboard = UIStoryboard(name: "Main", bundle: Bundle(for: type(of: self))) sut = storyboard.instantiateViewController(withIdentifier: "SampleViewController") as? SampleViewController // 如果你用的是XIB // sut = SampleViewController(nibName: "SampleViewController", bundle: Bundle(for: type(of: self))) // 关键:手动访问view属性,触发懒加载,让IBOutlet绑定完成 _ = sut?.view }
解释:ViewController的view是懒加载的,只有当你主动访问它时,系统才会去加载XIB/Storyboard里的界面元素,完成IBOutlet的绑定。
第二步:修正断言逻辑,测试isHidden状态
把原来的断言改成针对视图的隐藏状态:
func test_hideView() { // 先验证初始状态是显示的 XCTAssertFalse(sut?.sampleView.isHidden ?? true, "初始状态下sampleView应该是显示的") sut?.hideView() // 验证调用hideView后视图被隐藏 XCTAssertTrue(sut?.sampleView.isHidden ?? false, "调用hideView后sampleView应该被隐藏") } func test_showView() { // 先手动将视图设为隐藏,模拟初始状态 sut?.sampleView.isHidden = true XCTAssertTrue(sut?.sampleView.isHidden ?? false, "测试前sampleView应该被隐藏") sut?.showView() // 验证调用showView后视图被显示 XCTAssertFalse(sut?.sampleView.isHidden ?? true, "调用showView后sampleView应该被显示") }
方案二:用依赖注入解耦,更优雅的单元测试
如果想让测试完全脱离UI文件(XIB/Storyboard),可以用依赖注入的方式,把sampleView改成可外部传入的属性,这样测试时不需要依赖界面文件。
第一步:修改原ViewController代码
class SampleViewController: UIViewController { // 将IBOutlet改为可设置的属性,支持依赖注入 @IBOutlet weak var sampleView: UIView! { didSet { // 设置初始状态 sampleView.isHidden = false } } // 供测试使用的自定义初始化方法 init(sampleView: UIView) { self.sampleView = sampleView super.init(nibName: nil, bundle: nil) } // 必须实现的初始化方法,兼容Storyboard/XIB加载 required init?(coder: NSCoder) { super.init(coder: coder) } func hideView() { sampleView.isHidden = true } func showView() { sampleView.isHidden = false } }
第二步:编写测试用例
import XCTest @testable import unitUITestPractice class unitUITestPracticeTests: XCTestCase { var sut: SampleViewController? var testSampleView: UIView! override func setUp() { super.setUp() // 初始化测试用的视图,注入到ViewController中 testSampleView = UIView() sut = SampleViewController(sampleView: testSampleView) } override func tearDown() { sut = nil testSampleView = nil super.tearDown() } func test_hideView() { XCTAssertFalse(testSampleView.isHidden, "初始状态下sampleView应该是显示的") sut?.hideView() XCTAssertTrue(testSampleView.isHidden, "调用hideView后sampleView应该被隐藏") } func test_showView() { // 先设置为隐藏 testSampleView.isHidden = true XCTAssertTrue(testSampleView.isHidden, "测试前sampleView应该被隐藏") sut?.showView() XCTAssertFalse(testSampleView.isHidden, "调用showView后sampleView应该被显示") } }
这种方式的好处是测试完全独立于UI文件,运行更快,也避免了加载界面可能带来的额外问题。
内容的提问来源于stack exchange,提问作者SwiftyJD

