Search code examples
swiftui-testingtyphoon

Typhoon swap assembly for UI testing


Is it possible to swap or patch an assembly when running a UI test on iOS? Currently I have a project where I have setup Typhoon framework and I am able to patch an assembly like this

var controller: HomeViewController!

override func setUp() {
    super.setUp()
    // setup assemblies
    var blAssembly = BusinessLogicAssembly()
    var ctrlAssembly = AppAssembly()

    // setup patcher
    let patcher = TyphoonPatcher()
    patcher.patchDefinitionWithSelector("testManager", withObject: {return FakeManager()})

    patcher.patchDefinition(blAssembly.testManager() as TyphoonDefinition, withObject: {
        return FakeManager()})


    let factory = TyphoonBlockComponentFactory(assemblies: [blAssembly, ctrlAssembly])
    factory.attachPostProcessor(patcher)


    // get controller
    controller = factory.componentForKey("homeViewController") as HomeViewController

    // force view to laod
    let vcView = controller.view
}

And this is working fine. It patches the TestManager with a stub. But in this case I am manually invoking my view controller. When I run a UI test (where the controllers are handled behind the scenes when the app is launched) is there a way of patching an assembly and providing a mock/stub?

For instance lets say I have a view controller which is calling a web service. The web service logic is wrapped in a separate class and is injected into the view controller with a TyphoonAssembly. Now for my UI tests I do not want to contact my actual web services but just proved sample data. I am imagining doing this by creating a stub of my web service class and returning test data. Can this be achieved with the Typhoon framework because I was unable to do it or find an example anywhere.

I am using Swift but Objective-C answers will work too (as long as it is compatible)


Solution

  • Now there's a few ways with Typhoon that one component can be swapped for another. We could use TyphoonPatcher or modularize assemblies. Or we could even make a custom TyphoonDefinitionPostProcessor or TyphoonInstancePostProcessor.

    The problem/solution:

    But if I'm understanding your question correctly, the problem is that application-style (default) kind of tests are being used, where:

    • The TEST_HOST flag is set. Your application classes are visible to your tests.
    • Integration tests instantiate their own assembly, rather than use the application assembly. While this is normally recommended, its important to be aware that this is a separate, additional assembly to the one used in the main app.

    If you want to modify the main app's assembly from tests, we can use defaultFactory. If you're using plist integration:

    Create a definition for you app delegate:

     dynamic func appDelegate() -> AnyObject {
        return TyphoonDefinition.withClass(AppDelegate.self) {
            (definition) in
    
            //This is a public instance var of type TyphoonComponentFactory
            definition.injectProperty("assembly", with: self)
        }
    }    
    

    . . and then in your AppDelegate:

    func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
    
        factory.makeDefault()
    
    }
    

    . . . now in your tests or wherever you need, you patch the app's assembly, by getting a hook to it:

    TyphoonComponentFactory *factory = [TyphoonComponentFactory defaultFactory];
    [factory unload]; //clear out any singletons
    //Now patch it
    
    • Note that you may need to call factory.unload() to clear any instances of TyphoonScopeSingleton before-hand.
    • Also note that TyphoonPatcher also allows unpatching or winding back if you wish.

    Alternative:

    Alternatively, and simpler (we recommend simple wherever possible), perhaps you could just start up the app with a different network assembly specifying a different set of assemblies in the application plist.



    *NB: In Objective-C any TyphoonComponentFactory can be cast to an Assembly and vice-versa. This is not allowed in Swift, so might create some inconvenient limitations. To be addressed by #253