台风以编程方式加载故事板似乎可以在不阻塞的情况下执行异步实例化

Typhoon loading storyboard programmatically appears to perform asynchronous instantiation without blocking

我正在开发一个 iOS 应用程序并尝试将 Typhoon 集成到测试中。我目前正在尝试模拟来自情节提要的视图控制器中的依赖项,因此在我的程序集中:

public dynamic var systemComponents: SystemComponents!
public dynamic func storyboard() -> AnyObject {
    return TyphoonDefinition.withClass(TyphoonStoryboard.self) {
        (definition) in
        definition.useInitializer("storyboardWithName:factory:bundle:") {
            (initializer) in
            initializer.injectParameterWith("Main")
            initializer.injectParameterWith(self)
            initializer.injectParameterWith(NSBundle.mainBundle())
        }
    }
}

我想创建一个 CameraModeViewController(class 我正在单元测试),它依赖于提供系统相机功能的协议,已被模拟出来。依赖关系是 dynamic var cameraProvider: CameraAPIProvider?。我想我正确地创建了一个替换协作程序集来替换 systemComponents; MockSystemComponentsSystemComponents 的子 class,它覆盖了函数。这是我注入模拟的地方:

let assembly = ApplicationAssembly().activateWithCollaboratingAssemblies([
                            MockSystemComponents(camera: true)
                        ])
let storyboard = assembly.storyboard()
subject = storyboard.instantiateViewControllerWithIdentifier("Camera-Mode") as! CameraModeViewController

测试中的下一行代码是 let _ = subject.view,我学到的是调用 viewDidLoad 并获取所有故事板链接的 IBOutlets 的技巧,其中一个是此测试所必需的.

然而,我得到了非常神秘的结果:有时但并非总是如此,所有测试都会失败,因为在 viewDidLoad 中我调用了依赖项 (cameraProvider),然后我得到了一个"unrecognized message sent to class" 错误。该错误似乎表明在发送消息时(这是协议 CameraAPIProvider 中的正确实例方法),该字段当前是 CLASS 而不是实例:它将消息解释为 +[MockSystemCamera cameraStreamLayer] 如错误消息中所报告的那样。

~~~但是~~~

关键在于:如果我在 assembly.storyboard()subject.view 的调用之间添加一个断点,测试总是会通过。一切都设置正确,并且消息正确发送到没有这个 "class method" 虚假解释的实例。因此,我想知道台风是否在我必须等待的注入中做了某种异步过程?可能仅在处理情节提要交付的视图控制器时?如果是这样,有什么方法可以确保它阻塞?

在 Typhoon 的源代码中挖掘了一段时间后,我得到的印象是 TyphoonDefinition(Instance Builder) initializeInstanceWithArgs:factory: 方法中有一个 __block id instance 暂时是 Class类型,然后替换为该类型的实例;并且可能可以在不阻塞的情况下异步调用它,因此注入的成员保留为 Class 类型?


更新: 添加 MockSystemComponents(camera:) 的代码。请注意,SystemComponents 继承自 TyphoonAssembly.

@objc
public class MockSystemComponents: SystemComponents {
    var cameraAvailable: NSNumber

    init(camera: NSNumber) {
        self.cameraAvailable = camera
        super.init()
    }

    public override func systemCameraProvider() -> AnyObject {
        return TyphoonDefinition.withClass(MockSystemCamera.self) {
            (definition) in
            definition.useInitializer("initWithAvailable:") {
                (initializer) in
                initializer.injectParameterWith(self.cameraAvailable)
            }
        }
    }
}

更新 #2: 我尝试用 属性 注入替换 MockSystemComponents.systemCameraProvider() 中的构造函数注入。不同的问题,但我怀疑它在原因上是等价的:现在,注入的 属性(声明为可选)在我打开它的某些时候仍然是 nil(但并不总是——可能大约 4/5 的测试运行失败,与以前大致相同。


更新 #3: 已尝试使用以下代码块,根据 this answer (note that setting factory directly didn't work as that OP did, but I think I correctly used the feature added in response to Jasper's issue 使用工厂构造)。结果与使用 属性 注入时相同,如上面的更新#2),所以那里没有骰子。

这个问题实际上甚至在调用实例化之前就已经出现了。事实上,问题是程序集 通常 并不打算有状态。有几种方法可以解决这个问题,但不推荐使用我使用的方法——拥有一个成员变量和一个初始化方法。这样做的问题是,在 activateWithCollaboratingAssemblies 方法中,程序集的所有实例方法都被枚举用于定义,并且实际上将在协作程序集上调用初始化程序。因此,即使您使用初始化程序创建程序集,它也可能会被再次调用并使用虚假值。

请注意,出现异步行为的原因实际上是定义的组装顺序不确定(属性 将它们存储在 NSDictionary 中)。这意味着如果 activateWithCollaboratingAssemblies 碰巧枚举首先依赖于状态的方法,它们将工作正常;但是,如果先枚举初始化器,然后销毁状态,则之后创建的定义将变得乏味。