Search code examples
typhoon

Typhoon: How is patching a definition related to component keys?


I'm trying to implement a patcher as demonstrated in the documentation and this SO post: Typhoon: How to get an instance conforming to a protocol for production, and another for tests?.

I'm using block assembly and get the error:

[WPAnalyticsClientImplementation key]: unrecognized selector sent to instance 0x9eb01d0

at TyphoonPatcher.m: 46.

My class implementation does not respond to this selector. Should it? How are keys related to the patching process?

context(@"when the controller does something", ^{

        it(@"should work", ^{
            // This is an application test, so the factory has already been set in the app delegate.
            TyphoonComponentFactory *factory = [TyphoonComponentFactory defaultFactory];

            TyphoonPatcher* patcher = [[TyphoonPatcher alloc] init];
            [patcher patchDefinition:[factory componentForType:@protocol(WPAnalyticsClient)] withObject:^id
             {
                 id mockAnalytics = [KWMock mockForProtocol:@protocol(WPAnalyticsClient)];
                 [[mockAnalytics should] conformToProtocol:@protocol(WPAnalyticsClient)];
                 [mockAnalytics stub:@selector(getSomeString) andReturn:theValue(@"fake implementation")];

                 return mockAnalytics;
             }];

            [factory attachPostProcessor:patcher];

            // The default factory should now return the mocked client.
            id <WPAnalyticsClient> client = [factory componentForType:@protocol(WPAnalyticsClient)];

            NSLog(@"conforms: %i", [client conformsToProtocol:@protocol(WPAnalyticsClient)]);
            NSString *actualValue = [client getSomeString];
            NSLog(@"someString: %@", actualValue);
            [[theValue([actualValue isEqualToString:@"fake implementation"]) should] equal:theValue(YES)];
        });
    });

AppDelegate.m

TyphoonComponentFactory *factory = ([[TyphoonBlockComponentFactory alloc] initWithAssembly:[WPAssembly assembly]]);
[factory makeDefault];

Solution

  • The patching code shown above is not quite right, rather than patch an instance, you patch a definition.

    The way the patcher works is to use a TyphoonComponentFactoryPostProcessor to mutate a definition.

    So rather than doing this:

    [patcher patchDefinition:[factory componentForType:@protocol(WPAnalyticsClient)] 
        withObject:^id. . .
    

    You should do this:

    MyAssemblyType* assembly = [MyAssemblyType assembly];
    TyphoonComponentFactory* factory = [TyphoonBlockComponentFactory factoryWithAssembly:assembly];   
    [patcher patchDefinition:[assembly myComponentToPatch] withObject . . . ]; 
    

    Patching the Default Assembly::

    Because you're patching the default assembly, rather than creating a new one, you have to pass in the definition as follows:

    [patcher patchDefinition:[[MyAssemblyType assembly] myAnalticsService] withObject. . . ]
    

    Component Keys vs Assembly Interface

    Let's say you have a component as follows:

    - (id) myAnalyticsService 
    {
        return [TyphoonDefinitionWithClass. . . . etc]; 
    }
    

    . . . then the key of your component is @"myAnalyticsService" so you could also use:

    [patcher patchDefinitionWithKey:@"myAnalyticsService" . . ];
    

    Assembly Interface at Build-time vs Runtime

    Here's a concept that could cause some confusion:

    The assembly interface serves two purposes. At build-time it returns TyphoonDefinition, while at runtime it returns the actual type defined in the definition. So . .

    • At build time we can define components.

    • At run-time we can resolve components using the method name on the assembly interface

    Example:

    MyAssemblyType* assembly = (MyAssemblyType*) [TyphoonComponentFactory defaultFactory];
    //Use the assembly interface instead of a 'magic string'
    AnalyticsService* service = [assembly analyticsService]; 
    

    This is a lot of information . . . let me know if something is still not clear.