Skip to content

Integration Testing

Jasper Blues edited this page May 28, 2014 · 44 revisions

[Types of Injections](Types of Injections) | [What can be Injected](What can be Injected) | Modules | Scopes | Obtaining Built Components | Integration Testing

#First, a short discussion:

This section describes integration testing with Typhoon.

At the code-level (as opposed to an applications functional interface) we can perform two kinds of testing:

  • Unit Tests - this means testing a class in isolation from it's dependencies using test-doubles such as mocks or stubs.
  • Integration Tests - this means testing a class using real dependencies.

The value of unit tests has been recognized in the software community for a long time. Recently, there's been a resurgence of interest in integration testing. (Example, Ruby on Rails founder David Heinemeier Hansson recently blogged about this). In very short summary the value of each kind of testing is as follows:

Unit Tests Advantages:

  • Provide very good failure isolation
  • Ensure each component is fit-for-purpose before integrating
  • Fast

Unit Test Disadvantages:

  • Encourage glass-box testing as opposed to black-box testing. (ie mocks make it necessary to know what's going on inside a class, in order to test the external interface.
  • Can be costly to set up or support changes.

Integration Test Advantages:

  • Bang-for-your-buck: A small investment can provide very high test coverage
  • Ensures components are working together as they were intended
  • Encourages black-box style testing. Its not necessary to know what's going on inside a class. Just what's necessary to exercise the external API.

Integration Test Disadvantages:

  • Indicate only that there was a failure, and not where there was a failure.
  • Can be slow.
  • Can have real effects that need to be rolled back.
  • Can be difficult to put the system into the required state.

Using Dependency Injection to Configure Integration Tests:

The Dependency Injection design pattern is not only useful for unit testing, it provides a very powerful way to perform integration testing. Because an application's configuration is encapsulated in the assembly, its possible to replace just one or two components in order to support an integration test - putting the system into a required state, in order for a test to run. This could be swapping out the user manager to provide a pre-configured test account, for example.

#Supported Test Frameworks

Typhoon works with whatever test runners, matching libraries, and mocking libraries you like to use. Here's an example of using Typhoon with OCUnit:

@implementation LoyaltyCardDaoTests
{
    id <LoyaltyCardDao> _loyaltyCardDao;
}

- (void)setUp
{
    ApplicationAssembly* assembly = (ApplicationAssembly*) 
        [TyphoonBlockComponentFactory factoryWithAssembly:[ApplicationAssembly assembly]];
    _loyaltyCardDao = [assembly loyaltyCardDao];
}

- (void)test_should_allow_finding_loyalty_card_by_id
{
    
}

@end


#Patching Out A Component

You can patch out a component as follows:

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 = [factory componentForKey:@"knight"];

see also:Modules



#Asynchronous Integration Testing

It's very common for iOS & OSX applications to have components that collaborate with the main thread via background threads. One example is making a network request on a background thread, and then updating the user interface with new data when it completes.

Many test libraries now support asynchronous testing. Or, if you like, you can use Typhoon's simple and easy approach as follows:

- (void)test_should_retrieve_a_weather_report_given_a_valid_city
{
    __block WeatherReport* receivedReport;
    [weatherClient loadWeatherReportFor:@"Manila" onSuccess:^(WeatherReport* report)
    {
        receivedReport = report;
    }];

    
    [TyphoonTestUtils waitForCondition:^BOOL
    {
        typhoon_asynch_condition(receivedReport != nil);
    }];
}

If the condition does not occur before the time-out (default is seven seconds, checked very 1 second), then an exception will be raised, and the test will fail.


####Performing additional asynchronous assertions:

Once the initial condition has been set, you can perform extra assertions or logging as follows:

- (void)test_should_retrieve_a_weather_report_given_a_valid_city
{
    __block WeatherReport* receivedReport;
    [weatherClient loadWeatherReportFor:@"Manila" onSuccess:^(WeatherReport* report)
    {
        receivedReport = report;
    }];
    
    [TyphoonTestUtils waitForCondition:^BOOL
    {
        return receivedReport != nil;
    } andPerformTests^
    {
        //Use any kind of matchers that you like here - standard OCUnit, OCHamcrest, Kiwi, Expecta, etc. 
    }];
}

####Overriding default time-out.

You can override the default time-out (of seven seconds) using the following:

- (void)test_should_retrieve_a_weather_report_given_a_valid_city
{
    __block WeatherReport* receivedReport;
    [weatherClient loadWeatherReportFor:@"Manila" onSuccess:^(WeatherReport* report)
    {
        receivedReport = report;
    }];
    
    [TyphoonTestUtils wait:2 secondsForCondition:^BOOL
    {
        return receivedReport != nil;
    } andPerformTests^
    {
        //Tests 
    }];
}

  • Both unit and integration testing have value in a software project. It should not be all of one, and none of the other.
  • For unit-style testing, we recommend Jon Reid's OCMockito or OCMock3