Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make TyphoonPatcher add new components into a TyphoonComponentFactory, instead of only offering 'patching' of existing components #457

Open
fatuhoku opened this issue Nov 30, 2015 · 1 comment

Comments

@fatuhoku
Copy link
Contributor

Say I have an application with two assemblies: FooAssembly and BarAssembly. BarAssembly actually makes use of FooAssembly, so FooAssembly shouldn't know anything about BarAssembly.

In FooAssembly I declare a component called foo. foo has a fooExternalInterface property of type id <FooExternalInterface>. This is injected by type:

// FooAssembly.m

- (Foo *)foo {
    return [TyphoonDefinition withClass:[Foo class] configuration:^(TyphoonDefinition *definition) {
        [definition injectProperty:@selector(fooExternalInterface)];
    }];
}

// NB: no component called `fooExternalInterface` is declared in `FooAssembly`.

Let's say in BarAssembly we have a component bar that conforms to FooExternalInterface. When the application is put together, this dependency gets resolved. Happy days. This is a powerful technique for isolating assemblies from one another.

Now, let's say I want to write an integration test for FooAssembly only, following the guide over at https://github.com/appsquickly/typhoon/wiki/integration-testing. I only activate FooAssembly. In the absence of BarAssembly, Typhoon complains that there's no instances match the type id <FooExternalInterface>.

Well... okay, how can I inject a test double for this interface then? I refuse to have my test initialise BarAssembly.

Do I need to declare a BarTestAssembly that declare a test component? Nah, that's too much effort — it's a lot of code just to perform an injection.

So far, I came up with this solution:

// MyFooIntegrationTest.m

@interface FakeBar <FooExternalInterface>
//...
@end

@implementation FakeBar
//...
@end

- (void)setUp {
    [super setUp];
    TyphoonBlockComponentFactory *factory = [TyphoonBlockComponentFactory factoryWithAssembly:[FooAssembly assembly]];
    [factory registerDefinition:[TyphoonDefinition withClass:[FakeBar class]]]; // resolves against FooExternalInterface
    [factory attachPostProcessor:patcher];

    _sut = [(FooAssembly *) factory foo];
}

... but this is still not idea, because I just want to supply a OCMock object directly, so that I can verify interactions against it. I tried TyphoonDefinition's with:; it didn't work with injection by type.

Well, I thought TyphoonPatcher would be perfect for this — but there's a problem: fooExternalInterface is not a component that can be patched out in FooAssembly.

I really really wish I could say to the patcher, "Hey, please take this mock object, and resolve to it whenever you get asked about id <FooExternalInterface> when injecting by type. K thxbye!" that would be grand.

@jasperblues Any thoughts?

@fatuhoku
Copy link
Contributor Author

An alternative solution is to allow injection-by-type to be expressed explicitly with a component, through a TyphoonDefinition:

// FooAssembly.m

- (Foo *)foo {
    return [TyphoonDefinition withClass:[Foo class] configuration:^(TyphoonDefinition *definition) {
        [definition injectProperty:[self fooExternalInterface]];
    }];
}

// This component says "Some other assembly must supply an object that conforms to `FooExternalInterface`."
- (id <FooExternalInterface> *)fooExternalInterface {
    return [TyphoonDefinition withProtocol:@protocol(FooExternalInterface)];  // injection-by-protocol-type
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants