【问题标题】:deinit called in app but not in unit test when dismissing view controller关闭视图控制器时在应用程序中调用 deinit 但在单元测试中未调用
【发布时间】:2019-09-14 07:18:57
【问题描述】:

我正在尝试对视图控制器的内存进行单元测试,以查看它们在被解除时是否正确取消初始化。

class SettingsViewControllerTests: XCTestCase {

    var controller: SettingsViewController!

    override func setUp() {
        super.setUp()

        let storyboard = UIStoryboard(name: "Main", bundle: nil)
        controller = storyboard.instantiateViewController(withIdentifier: "SettingsViewController")
            as? SettingsViewController

        //load view hierarchy
        _ = controller.view
    }

    func testLogout() {
        let sideMenu = MockSideMenuViewController()
        var navController: UINavigationController? = UINavigationController(rootViewController: sideMenu)
        sideMenu.show(navController!, sender: nil)
        navController?.pushViewController(controller, animated: true)
        expect(navController).toNot(beNil())
        controller.dismiss(animated: false, completion: nil)

        expect(navController).toEventually(beNil(), timeout: 3) // fails
        expect(self.controller).toEventually(beNil(), timeout: 3) // fails
    }

在应用程序中,我使用 segue 来展示我的导航控制器 + SettingsViewController。它有一个调用 dismiss 的方法,但是当我检查我的控制器实例时,它们仍然存在。在我的视图控制器中,我设置了一个打印语句来检查是否调用了 deinit,当我完成应用程序上的步骤时,它确实被调用(两个控制器最终都转到nil)。然而,单元测试并没有做同样的事情。我错过了什么?

【问题讨论】:

    标签: ios swift unit-testing uikit


    【解决方案1】:

    controller 是一个强持有的属性,所以你的测试会保留它。现在它只会在后续调用setUp() 时被取消初始化。如果您想专门测试controller.deinit,请在测试中执行controller = nil

    你可能会做这样的事情:

    var controller: SettingsViewController!  // <--- this is a strong ref
    
    func testLogout() {
            let sideMenu = MockSideMenuViewController()
    
            // navController is a strong ref, held until the end of the scope; don't expect it to be nil
            var navController: UINavigationController? = UINavigationController(rootViewController: sideMenu)
            sideMenu.show(navController!, sender: nil)
            navController?.pushViewController(controller, animated: true)
    
            // hold a weak ref to your controller and then nil out its reference
            weak var weakController = controller
    
            // remove the strong reference
            controller = nil
    
            // popping will release the last reference 
            navController?.popViewController(animated: false)
    
            expect(weakController).to(beNil(),) // succeeds
        }
    

    一些注意事项:

    • navController 是范围级别的变量。在函数结束之前它不会是 nil,所以没有理由测试或期望它。在你创建它之后它也肯定不会是 nil。

    • UIViewController.dismiss(...) 用于解除模式。您的控制器是导航堆栈的一部分。解雇不会如你所愿。

    感觉就像您期望var navController: UINavigationController? 很弱,但事实并非如此。 weak 变量和属性应该是 Optional,但可选并不意味着 weak。见Weak References

    【讨论】:

    • dismiss 不是将控制器设置为 nil 吗?在应用程序实例中,我不必手动将我的视图控制器设置为nil。我的控制器在哪里被引用,以便为我设置为nil?因为在该代码中,我什至不必调用 weakController.dismiss 即可通过测试用例。
    • 在上面添加了一些额外的注释。解雇不会将其设置为零。它消除了一个模态。另请注意,即使它设置了一些对 nil 的引用,您仍然有另一个未设置为 nil 的引用。它不会影响您对同一对象的其他引用。
    • 我删除了解雇的电话。在提交之前应该更好地校对我的回复哈哈。希望注释和编辑能澄清一点。
    • 另外,这个测试实现了什么?你认为你在某个地方有一个保留周期吗?
    • 请注意,窗口和路由器都不是测试的一部分。你的测试基本上不会测试这个。至少,不像写的那样。假设它不在导航堆栈上,它也将不可见。
    猜你喜欢
    • 2017-10-06
    • 2020-08-12
    • 2016-07-05
    • 1970-01-01
    • 2019-05-19
    • 2019-08-08
    • 2018-05-24
    • 2017-01-18
    • 2021-12-16
    相关资源
    最近更新 更多