TyphoonPatcher for mocking in unit tests

回眸只為那壹抹淺笑 提交于 2019-12-10 22:58:02

问题


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?


回答1:


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.



回答2:


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


来源:https://stackoverflow.com/questions/24956211/typhoonpatcher-for-mocking-in-unit-tests

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!