appsquickly / typhoon

Powerful dependency injection for Objective-C ✨✨ (https://PILGRIM.PH is the pure Swift successor to Typhoon!!)✨✨
https://pilgrim.ph
Apache License 2.0
2.7k stars 269 forks source link

Swift, PList Integration, Storyboard, XCTest - autowiring issues. #394

Closed achansonjr closed 9 years ago

achansonjr commented 9 years ago

So first and foremost, thanks for a great DI framework. I am really enjoying using it.

Now for the question.

Environment: XCode-Beta iOS 9 Unit Test Framework: XCTest Typhoon Features Used: PList Integration, Storyboard Integration, Modular Assemblies

import Typhoon
import CoreData

public class PersistenceTestAssembly : TyphoonAssembly {

  var moc : NSManagedObjectContext
  override init() {
    do {
      moc = try setUpInMemoryManagedObjectContext()
    } catch {
      exit(0)
    }
  }

  public dynamic func objectPAL() -> AnyObject {
    return TyphoonDefinition.withClass(ObjectPAL.self) {
      (definition) in

      definition.useInitializer("initWithMoc:") {
        (initializer) in

        initializer.injectParameterWith(self.moc)
      }
      definition.scope = TyphoonScope.Singleton
    }
  }
}

So this is the test specific assembly that I have set up to insert a test specific MOC for our data access objects (what we are calling PAL) here.

In the main application we are using PList integration, with a production version of the assembly above (as well as an application assembly) and everything works great. The storyboard is converted to a Typhoon storyboard, the controllers and associated stuff is injected properly.

Now here is where I am having issues. I have created a plist for the test target, using the same values as the plist in the main target. I imagine that this is incorrect, however I figured that since the test target uses the appDelegate from the main target to run tests, it might be a good start. I didn't want to willy-nilly create a separate applicationTestAssembly in the test target as I didn't know if I could still tie that to the main targets appDelegate.

With this configuration I never could get the test target to initialize. Here is what I have managed to get working in my test using manual injection.

import XCTest
@testable import MainProject
import CoreData

class ObjectsTableViewCellTests: XCTestCase {

  var uut: ObjectsTableViewCell!
  var moc: NSManagedObjectContext!
  var obj: Object!

  override func setUp() {
    super.setUp()
    // Put setup code here. This method is called before the invocation of each test method in the class.
    let storyboard = UIStoryboard(name: "Main", bundle: nil)
    let assembly = PersistenceTestAssembly().activate()
    let libraryController = storyboard.instantiateInitialViewController() as! LibraryViewController
    let objectNavigationController = containerController.viewControllers?[0] as! UINavigationController
    let objectsTableViewController = objectNavigationController.topViewController as! ObjectsTableViewController

    objectsTableViewController.objectPAL = assembly.objectPAL() as! ObjectPAL
    uut = objectsTableViewController.tableView.dequeueReusableCellWithIdentifier("ObjectCell") as! ObjectsTableViewCell
  }
}

I have truncated the actual tests, but the crux of the issue is that this is the only way that I could get a typhoon created object in the assembly assigned to the specific controllers usually autowired variable.

Is there a way to do this with plist integration?

I tried to manually do it like so:

import XCTest
@testable import MainProject
import CoreData

class ObjectsTableViewCellTests: XCTestCase {

  var uut: ObjectsTableViewCell!
  var moc: NSManagedObjectContext!
  var obj: Object!

  override func setUp() {
    super.setUp()
    // Put setup code here. This method is called before the invocation of each test method in the class.
    let bundle : NSBundle = NSBundle(forClass: ObjectsTableViewController.self)
    let assembly = PersistenceTestAssembly().activate()
    let factory : TyphoonComponentFactory = TyphoonBlockComponentFactory(assembly: assembly)
    let storyboard = TyphoonStoryboard(name: "Main", factory: factory, bundle: bundle)
    let containerController = storyboard.instantiateInitialViewController() as! ContainerViewController
    let objectNavigationController = containerController.viewControllers?[0] as! UINavigationController
    let objectsTableViewController = objectNavigationController.topViewController as! ObjectsTableViewController
    objectsTableViewController.objectPAL = assembly.objectPAL() as! ObjectPAL
    uut = objectsTableViewController.tableView.dequeueReusableCellWithIdentifier("ObjectCell") as! ObjectsTableViewCell
  }
}

I assumed by creating the factory, and making storyboard an instance of TyphoonStoryboard that the plist integration might be picked up, but that is likely a false assumption as it would make sense that there is some larger lifecycle events that are happening surrounding the appDelegate insertion when Typhoon squirms its way in through the plist integration.

What would you suggest that I do regarding wanting to use the autoinjection capability of Typhoon to autowire test specific dependencies using plist integration? I would really like to try and mimic the production features in my tests to ensure that version changes should I pick up a new version of the Pod won't break my use of the PList and Storyboard integration features.

Thanks for your time.

mogol commented 9 years ago

You could create factory with from Info.plist using

[TyphoonBlockComponentFactory factoryFromPlistInBundle:[NSBundle mainBundle]]

Also you could use Patch feature to modify factories.

alexgarbarev commented 9 years ago

Hello, Plist integration is not supported in unit tests, because there is completely different life cycle: in app we creating factory once at startup, then use across the app, in tests we have to create factory for each test.

If you want to test your ViewControllers with autowire feature, just create TyphoonStoryboard instead of UIStoryboard in your test (like in your latest snippet)

if you want to test Typhoon's plist integration, you can create your factory using:

NSBundle *myTestBundle = [NSBundle bundleForClass:[MyTest class]];
TyphoonBlockComponentFactory *factory = [TyphoonBlockComponentFactory factoryFromPlistInBundle:myTestBundle];
TyphoonStoryboard *storyboard = [TyphoonStoryboard storyboardWithName:@"Main" factory:factory bundle:myTestBundle];

(please convert that code into swift)

But if you afraid that Plist integration would be broken in future Typhoon version, we have unit tests for that case (Current coverage of TyphoonStartup class is 76%, TyphoonInitialStoryboardResolver: 88%) If you find these tests not useful, please add your unit tests into Typhoon and create PullRequest. (we will be happy)

achansonjr commented 9 years ago

Thank you very much for your responses.

Its nice to know I wasn't completely off base regarding the different life cycles. I'll implement as you describe and will make time to create a test in the swift example that will show off how to do this.

Thanks again.

jasperblues commented 9 years ago

Hello @achansonjr,

Just a few extra notes:

Integration Testing

The reason that we recommend instantiating a unique instance of Typhoon for each test, is that when testing with Typhoon in this way, we are doing integration testing - that is, testing a class using real collaborators, as opposed to mock or stub dependencies. If we used the same instance, then behaviors of one test, might effect the results of another.

However, if you wished to take a chance, and use the same instance of Typhoon as the app, you could use [TyphoonComponentFactory defaultFactory].

Did you read Typhoon's guide on Integration Testing in the user manual?

Loading from a plist

You can load the same assembly as the one specified in a plist with + (id)factoryFromPlistInBundle:(NSBundle*)bundle in TyphoonBlockComponentFactory

Note that, as @alexgarbarev states, this does not swizzle regular storyboards for Typhoon storyboards, as that is a feature of plist style bootstrapping, and not just the fact that an instance of Typhoon was loaded from a plist.

You can use auto-injection in tests

Just declare the properties with auto-injection macros in your test. Put this in setUp:

- (void)setUp
{
    [[[ApplicationAssembly new] activate] inject:self];
}

Hopefully now you have enough information to proceed. Thankyou @appsquickly/typhoon-contributors - many hands make light work.

achansonjr commented 9 years ago

@jasperblues Thanks for the added input. I did read the integration testing portion of the documentation, but I'm still learning all the different portions of the stuff under the hood. Since I've had such good luck with everything just working at a relatively high abstraction level, this was my first foray into trying to use the more low level stuff.

Digging into the factories and their lifecycles, as well as the injection mechanisms is still a little fuzzy. You, @alexgarbarev, and @mogol have all helped to shine more light on the issue. Even helping me to realize that if I want to test the plist integration, I shouldn't do that in a view test that is so specific.

Thanks again for making Typhoon. It's really been great help in my project.

alexgarbarev commented 9 years ago

@achansonjr Can you share code of autwire feature in swift? I didn't know it's possible, because it uses macroses.

update: Ah.. I was confused with terms. I meant auto-injection, like

@property (nonatomic, strong) InjectedProtocol(MyService) service;

because it was called autowire previously. Do you mean injection properties by its type?

achansonjr commented 9 years ago

@alexgarbarev Hey alex. Yeah, I meant injection of properties by their type. Or rather, whatever happens when using plist integration to get Typhoon to inject without each controller manually invoking the assembly. I am currently using Typhoon + Plist integration + Swift so that I define my assemblies, name them in the TyphoonInitialAssemblies array, and everything is just injected automagically.

Does that answer your question?

alexgarbarev commented 9 years ago

@achansonjr yes, thanks for clarification

jasperblues commented 9 years ago

@achansonjr That feature is indeed called auto-wiring, and works with both Plist Inegration or assembly activation.

There's also auto-injection macros for Objective-C. . . but we managed to confuse ourselves by sometimes informally calling that auto-wiring.

achansonjr commented 9 years ago

@alexgarbarev No problem. Its neat to learn that Typhoon injects by type. I wasn't sure if it was possibly using reflection to find the proper matching assembly definition.

I just did some extra reading on the difference between auto-injection and auto-wiring. I am assuming that the auto-injection is what you meant regarding "injecting by type." If auto-injection (in Obj-C) is strictly by type, what specifically is the mechanism when you use auto-wiring. Is it some form of reflection?

@jasperblues Thanks for the clarification. Theres a lot here to digest, and I haven't had time to dig in, but enjoying what I am finding so far.

jasperblues commented 9 years ago

@achansonjr

Yes we use the Objective-C run-time and reflection to match by type. If the property is a protocol, Typhoon will look for an object that conforms to that protocol, ditto for a class. If there's more than one match an exception will be raised. If you're familiar with Spring for Java, this behavior is similar.

The Objective-C run-time only provides type information for properties, so initializers and methods must be wired explicitly.

jasperblues commented 9 years ago

A little bit of info about how Typhoon works under the hood: https://github.com/appsquickly/Typhoon/wiki/TyphoonAssembly

achansonjr commented 9 years ago

@jasperblues That link helped a ton. Yeah all of my experience was with Spring so its probably why Typhoon was so familiar.

achansonjr commented 9 years ago

@jasperblues Actually now that I think about it, thats probably why I was focused on trying to use two different plist definitions for production and test instances. Springs ability to load two different bean definitions depending on the environment was how I remembered creating the various instances of the objects I wanted to inject, whether they were mocks or some other decoupled test specific implementation.

jasperblues commented 9 years ago

You can do the same kind of thing with Typhoon using Assembly Modularization: https://github.com/appsquickly/Typhoon/wiki/Modularizing-Assemblies

Two other features that might be useful are:

Other Things You'll Find Familiar

Spring has BeanFactoryPostProcessors and BeanPostProcessors, which can be used creatively. The analogs for Typhoon are described here: https://github.com/appsquickly/Typhoon/wiki/Infrastructure-Components . . we use them for various features under the hood, but like Spring, they can be applied by end users as well.

mogol commented 9 years ago

@achansonjr, also you could use preprocessor plist feature in Xcode: define different preprocessor files for different schemes (eg, Release, Debug) , in this files setup some define macro, eg typhoon assembly class name, use this macro in Info.plist in TyphoonInitialAssemblies