TyphoonPatcher for mocking in unit tests

2019-07-20 18:59发布

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?

2条回答
贼婆χ
2楼-- · 2019-07-20 19:09

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.
查看更多
欢心
3楼-- · 2019-07-20 19:30

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;
}];
查看更多
登录 后发表回答