I am developing an iOS application and am trying to integrate Typhoon into the testing. I am currently trying to mock out a dependency in a view controller that comes from the storyboard, so with in my assembly:
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())
}
}
}
I want to create a CameraModeViewController
(the class I am unit testing) with its dependency upon a system-camera-functions-providing protocol mocked out. The dependency is dynamic var cameraProvider: CameraAPIProvider?
. I think I correctly created a replacement collaborating assembly to replace systemComponents
; MockSystemComponents
is a subclass of SystemComponents
that overrides functions. This is where I inject the mock:
let assembly = ApplicationAssembly().activateWithCollaboratingAssemblies([
MockSystemComponents(camera: true)
])
let storyboard = assembly.storyboard()
subject = storyboard.instantiateViewControllerWithIdentifier("Camera-Mode") as! CameraModeViewController
The next line of code in the tests is let _ = subject.view
, which I learned is a trick to call viewDidLoad
and get all the storyboard-linked IBOutlets, one of which is required for this test.
However, I am getting very mysterious result: sometimes but not always, all the tests fail because in the viewDidLoad
I make a call to the dependency (cameraProvider
), and I get an "unrecognized message sent to class" error. The error seems to indicate that at the time the message is sent (which is a correct instance method in protocol CameraAPIProvider
) the field is currently a CLASS and not an instance: it interprets the message as +[MockSystemCamera cameraStreamLayer]
as reported in the error message.
~~~BUT~~~
Here's the kicker: if I add a breakpoint between the calls to assembly.storyboard()
and subject.view
, the tests always pass. Everything is set up correctly, and the message is correctly sent to an instance without this "class method" bogus interpretation. Therefore, I have to wonder if Typhoon does some kind of asynchronous procedure in the injection that I have to wait for? Possibly only when dealing with storyboard-delivered view controllers? And if so, is there any way to make sure it blocks?
After digging around in Typhoon's source for a while, I get the impression that in the TyphoonDefinition(Instance Builder)
initializeInstanceWithArgs:factory:
method there is an __block id instance
that is temporarily a Class type, and then is replaced with an instance of that type; and possibly this can be called asynchronously without blocking, so the injected member is left as a Class type?
UPDATE: Adding the code for MockSystemComponents(camera:)
. Note that SystemComponents inherits from 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)
}
}
}
}
UPDATE #2: I tried replacing the constructor injection in the MockSystemComponents.systemCameraProvider()
with a property injection. Different issue, but I suspect it's equivalent in cause: now, the property that is injected (declared optional) is still nil
some of the time when I go to unwrap it (but not always -- probably about 4/5 of test runs fail, about the same as before).
UPDATE #3: have tried using the following code block, using factory construction according to 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). The results are the same as when using property injection like Update #2 above), so no dice there.