Skip to content

Integration Testing

Jasper Blues edited this page Feb 7, 2019 · 44 revisions

In this section . . .

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

What component factory should my tests use?

Whatever test framework you're using, we recommend that tests use their own TyphoonComponentFactory (in setUp, before block, etc) and not borrow the one from AppDelegate. This makes it easy to configure the assembly for testing. As most tests will use the same assembly, you can create a category or utility method to load the integration test assembly.



Patching Out A Component

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

And in Swift:

let assembly = MiddleAgesAssembly().activate()
let patcher = TyphoonPatcher()
patcher.patchDefinitionWithSelector("knight") {
    let knight = FakeKnight()  // a manually mocked or fake class
    // ... set up your expectations on knight
    return knight;
}
assembly.attachPostProcessor(patcher)

see also: Modularizing Assemblies



Asynchronous Integration Testing

NB: Starting with Xcode6, the standard tool-set provides for asynchronous integration testing with XCTestExpectation.

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 OCMock 3