I have Assembly
:
@interface MDUIAssembly : TyphoonAssembly
@property (nonatomic, strong, readonly) MDServiceAssembly *services;
@property (nonatomic, strong, readonly) MDModelAssembly *models;
- (id)choiceController;
@end
@implementation MDUIAssembly
- (void)resolveCollaboratingAssemblies
{
_services = [TyphoonCollaboratingAssemblyProxy proxy];
_models = [TyphoonCollaboratingAssemblyProxy proxy];
}
- (id)choiceController
{
return [TyphoonDefinition withClass:[MDChoiceViewController class]
configuration: ^(TyphoonDefinition *definition) {
[definition useInitializer:@selector(initWithAnalytics:diary:)
parameters: ^(TyphoonMethod *initializer) {
[initializer injectParameterWith:[_services analytics]];
[initializer injectParameterWith:[_models diary]];
}];
}];
}
@end
Here what I'm trying to do in tests:
- (void)setUp
{
patcher = [TyphoonPatcher new];
MDUIAssembly *ui = (id) [TyphoonComponentFactory defaultFactory];
[patcher patchDefinition:[ui choiceController] withObject:^id{
return mock([MDChoiceViewController class]);
}];
[[TyphoonComponentFactory defaultFactory] attachPostProcessor:patcher];
}
- (void) tearDown
{
[super tearDown];
[patcher rollback];
}
Unfortunately my setUp
fails with next message:
-[MDChoiceViewController key]: unrecognized selector sent to instance 0xbb8aaf0
What I'm doing wrong?
Here's some extra advice to go along with the main answer . . .
Unit Tests vs Integration Tests:
In Typhoon we adhere to the traditional terms:
Unit Tests : Testing your class in isolation from collaborators. This is where you inject test doubles like mocks or stubs in place of all of the real dependencies.
Integration Tests: Testing your class using real collaborators. Although you may patch our a component in order to put the system in the required state for that test.
So any test that uses TyphoonPatcher
would probably be an integration test.
More info here: Typhoon Integration Testing
Resolve Collaborating Assemblies:
This was required in earlier version of Typhoon, but is not longer needed. Any properties that are are sub-class of TyphoonAssembly will be treated as collaborating assemblies. Remove the following:
- (void)resolveCollaboratingAssemblies
{
_services = [TyphoonCollaboratingAssemblyProxy proxy];
_models = [TyphoonCollaboratingAssemblyProxy proxy];
}
Tests instantiate their own assembly:
We recommend that tests instantiate and tear down their on TyphoonComponentFactory. The advantages are:
[TyphoonComponentFactory defaultFactory]
is a global and has some drawbacks.
- Integration tests can define their own patches without having to worry about putting the system back in the original state.
- In addition to using TyphoonPatcher, if you wish you can create an assembly where the definitions for some components are overridden.
You've encountered a poor design choice on Typhoon's part, but there's an easy work-around.
You're using this method:
[patcher patchDefinition:[ui choiceController] withObject:^id{
return mock([MDChoiceViewController class]);
}];
. . which is expecting a TyphoonDefinition
as argument. When bootstrapping Typhoon:
- We start with one ore more
TyphoonAssembly
subclasses, which Typhoon instruments to obtain recipes for building components. These TyphoonAssembly
sub-clases are then discarded.
- We now have a
TyphoonComponentFactory
that will allow any of your TyphoonAssembly
interfaces to pose in front of it. (This is so you can have multiple configs of the same class, while still avoiding magic strings, allows auto-completion in your IDE, etc).
When the TyphoonPatcher
was written it was designed for the case where you obtain a new TyphoonComponentFactory
for your tests (recommended), like this:
//This is an actual TyphoonAssembly not the factory posing as an assembly
MiddleAgesAssembly* assembly = [MiddleAgesAssembly assembly];
TyphoonComponentFactory* factory = [TyphoonBlockComponentFactory factoryWithAssembly:assembly];
TyphoonPatcher* patcher = [[TyphoonPatcher alloc] init];
[patcher patchDefinition:[assembly knight] withObject:^id
{
Knight* mockKnight = mock([Knight class]);
[given([mockKnight favoriteDamsels]) willReturn:@[
@"Mary",
@"Janezzz"
]];
return mockKnight;
}];
[factory attachPostProcessor:patcher];
Knight* knight = [(MiddleAgesAssembly*) factory knight];
What happened:
So the problem is that the TyphoonPatcher
is expecting TyphoonDefinition
from the TyphoonAssembly
and instead it is getting an actual component from a TyphoonComponentFactory
.
Very confusing, and that way of obtaining a patcher should be deprecated.
Solution:
Use the following instead:
[patcher patchDefinitionWithSelector:@selector(myController) withObject:^id{
return myFakeController;
}];