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];
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.