Search code examples
iosobjective-cintegration-testingdependency-managementtyphoon

How to inject fake, stubbed or mock dependencies for Integration tests using Typhoon


I'm trying to write integration tests using KIF. My question is:

How to inject stubbed, mock or fake dependency for particular view controller?

Each view controller using dependencies like a data model, http client, store manager etc. comes from ModelAssembly, ApplicationAssembly, ManagerAssmebly.

On storyboard, for login view i have a key path, containing value "loginViewController".

Creating view controllers:

ViewControllersAssembly.h

@interface ViewControllersAssembly : TyphoonAssembly
@property (nonatomic, strong) ModelAssembly *modelAssembly;

- (id)loginViewController;
@end

ViewControllersAssembly.m

@implementation ViewControllersAssembly
- (UIViewController *)loginViewController {
return [TyphoonDefinition withClass:[LoginViewController class] configuration:^(TyphoonDefinition *definition) {
    [definition injectProperty:@selector(userModel) with:[self.modelAssembly userModel]];
}];

}

UserModel have method to login

- (RACSingnal*)loginWithEmail:(NSString*)email password:(NSString*)password;

Now in integration tests target i have class like:

LoginTests.h

@interface LoginTests : KIFTestCase
@property (nonatomic, strong) UserModel *fakeUserModel;
@end

LoginTests.m

@implementation LoginTests

- (void)beforeAll {
    self.fakeDataModel = [self mockDataModel];
}

- (void)testLogin {
    [self.fakeDataModel mockNextResponse:[RACSignalHelper getGeneralErrorSignalWithError:[[NSError alloc] initWithDomain:@"http://some.com" code:452 userInfo:nil]]];
    [tester waitForViewWithAccessibilityLabel:@"loginScreen"];

    [tester enterText:@"user@gmail.com" intoViewWithAccessibilityLabel:@"emailAdress"];
    [tester enterText:@"asd123" intoViewWithAccessibilityLabel:@"password"];
    [tester tapViewWithAccessibilityLabel:@"loginButton"];

    [tester tapViewWithAccessibilityLabel:@"OK"];
    // for example error code 542 we should display alert with message "User Banned"
    // now somehow check that UIAlertView localizedDescription was "User Banned" 
}

- (FakeUserModel *)mockUserModel {
    ModelAssembly *modelAssembly = [[ModelAssembly assembly] activate];
    TyphoonPatcher *patcher = [[TyphoonPatcher alloc] init];
    [patcher patchDefinitionWithSelector:@selector(userModel) withObject:^id{
        return [FakeUserModel new];
     }];

    [modelAssembly attachDefinitionPostProcessor:patcher];
    return [modelAssembly userModel];
}

FakeUserModel is class that override UserModel class, adding possibility to stub response for next called request.

that solution not is not working.

How and where i should pass FakeUserModel?

1) i'd like to have access to injected instance

2) injected instance must be of type FakeUserModel, which is only in integration tests target.

3) i don't want to modify production code for integration tests.


Solution

  • Unit Testing

    If you wish to replace all dependencies for a given class with a test double, and thus test a class in isolation from its collaborators, this would be a unit test. Simply instantiate an instance for testing, passing in your test doubles (mock, stub, etc) as collaborators.

    Integration Testing

    If you wish to patch-out one or more instances in an assembly with a test double, to put the system into the required state for an integration test, Typhoon provides several approaches.

    You can patch out a component as follows:

    MiddleAgesAssembly* assembly = [[MiddleAgesAssembly assembly] activate];
    
    TyphoonPatcher* patcher = [[TyphoonPatcher alloc] init];
    [patcher patchDefinitionWithSelector:@selector(knight) withObject:^id{
        Knight* mockKnight = mock([Knight class]);
        [given([mockKnight favoriteDamsels]) willReturn:@[
            @"Mary",
            @"Janezzz"
        ]];
    
        return mockKnight;
    
    }];
    
    [assembly attachPostProcessor:patcher];
    
    Knight* knight = [(MiddleAgesAssembly*) factory knight]
    

    More information on this approach can be found in the Integration Testing section of the user guide.

    Modularization

    Alternatively you could modularize your assembly, and activate with a sub-class or alternative implementation, that provides another implementation of certain classes, example:

    UIAssembly *uiAssembly = [[UIAssembly new] 
        activateWithCollaboratingAssemblies:@[
            [TestNetworkComponents new], //<--- Patched for testing
            [PersistenceComponents new]];
    
    SignUpViewController* viewController = [uiAssembly signUpViewController];
    

    More information on this approach can be found in the modularization section of the user guide.