TDD in Objective-C: Scoping an iOS Project via Mocks and Protocols with Unit Tests in Kiwi

Overview

The following is an overview of how I approach Test Driven Development (TDD) for iOS projects. I use Kiwi as my TDD tool of choice, but you could apply the following with other TDD tools if you like. The only prerequisite is that the TDD tool supports mocks on protocols.

App Requirements

For this example I’m building an application to record the users location and transmit the results to a server. The basic requirements at this stage are as follows:

  1. Use CoreLocation to generate location information
  2. Filter raw location events to produce meaningful position
  3. Write locations to persistent storage
  4. Trigger upload of location data to a remote server

It’s worth noting that CoreLocation produces results with varying accuracy and the location data can come out of sequence. Therefore filtering is necessary to remove extraneous inputs. Also, running high resolution location services continuously severely impacts battery life, so a way of reducing use of the hardware is prudent.

Development Approach

The main thrust of this tutorial is how you can use TDD and BDD (Behavioural Driven Development) practices to produce a clean architecture for your app. It’s very tempting to start chucking in SDK calls to get things working, but you’ll quickly end up with a mess of code that is hard to test and hard to refactor, so getting the approach right up front is going to help you no end. No project has hard and fast requirements, they always develop as a result of test and customer feedback, so you don’t want a pile of spaghetti, even if it is supported by unit tests.

I’m going to use protocols to define the interfaces between objects, and write appropriate tests to demonstrate that each class uses those interfaces to meet the application requirements. I’m going to avoid creating class implementations and their unit tests until I need to focus on them. I find this helps to keep thinking clearly about the unit under test, rather than being dragged this way and that creating tests for units that aren’t yet needed and may not even come into being as your design is flesh out.

Starting the Project

First off you should create a new Xcode project targeted for iPhone. You should use the ‘Empty Application’ template, as there is no need for UI at this stage. When presented with the project features to enable, disable Core Data but leave ARC, Unit Testing and its always a good idea to create a local git repository. I’ve chosen not to include a class prefix for this tutorial, so your AppDelegate for example might be called XYZAppDelegate.

The resulting project should only contain the AppDelegate.h and .m files with minimal boiler plate code. If you build and run at this stage, you’ll get a white screen, no activity, and a note in the Xcode Console stating that apps are expected to have established a root view controller. That isn’t a problem for now, and your unit tests will run faster without an unnecessary UI to create.

You will also need to add Kiwi to the project. Follow the instruction in the Kiwi Wiki for how to add Kiwi to your project, I find Cocoapods the easiest way at the time of writing (with Kiwi at v2.2.2)

Writing Your First Spec

The first thing we should do is create a test spec that outlines our application requirements. We are going to expect the AppDelegate to coordinate the needs of the project. That may change later as the requirements flesh out to include UI, but the AppDelegate is a reasonable place to start.

Create a new .m in your unit test target group and call is AppDelegateTests.m.

I like to start off with some simple boiler plate that outlines the spec

#import "Kiwi.h"
#import "AppDelegate.h"

SPEC_BEGIN(AppDelegateSpec)

describe(@"AppDelegate", ^{

    __block AppDelegate *sut;

    beforeEach(^{

        sut = [[AppDelegate alloc] init];

    });

    afterEach(^{

        sut = nil;
    });

    it(@"should exist", ^{

        [sut shouldNotBeNil];

    });

});

SPEC_END

A couple of comments on the boiler plate:

  1. sut refers to the ‘system under test’. I prefer to use that as the target for the tests, rather than something more object specific such as appDelegate because it means I can refactor and move tests into new classes later if needed.
  2. The first expectation ensures that the class exists and can be created with a simple alloc and init. We may of course adapt this to our needs later, but I find its a good place to start.

This should already build and run as the AppDelegate was created for us.

Expressing the App Requirements

The next step is to add in some placeholders for the app requirements. You can pop these in after the first it expectation block.

context(@"Gets notified by the location service when new locations arrive", ^{

});

context(@"Passes new locations to the location filter", ^{

});

context(@"Gets notified by the location filter when locations are accepted", ^{

});

context(@"Writes accepted locations to storage", ^{

});

context(@"Notifies communications service that there are locations to upload", ^{

});

Here we’ve not actually created any tests, but we’ve expressed our app requirements. Into each of these contexts we can start to add expectations for how the app will meet those requirements.

It’s worth noting that we are intending to follow the ‘Single Responsibility Principle’ here, making sure that each area of the apps functionality is contained within its own class. Our AppDelegate is taking on the role of broker between the various components, setting up the environment and creating the adapters needed to move data between each area. I dare say there is refinement that can take place as the project develops, but we need to start somewhere!

Defining the Location Service

The iOS SDK includes the CoreLocation API to provide our app with a way of receiving location events. But rather than get into the nitty gritty of how that works at the moment, we are going to define our own ‘Location Service’ class that handles the lower level implementation.

Our context for tests is ‘gets notified by the location service…’, which means we must have an expectation that a location service exists. So we write an expectation that tests to see if the AppDelegate provides us with a reference to a Location Service.

context(@"Gets notified by the location service when new locations arrive", ^{

    it(@"has a locationService", ^{

        [[sut should] respondToSelector:@selector(locationService)];

    });
});

It doesn’t matter for now whether you choose to implement the location service as a property or method that returns the object, although I’m going for a read only property which does much the same thing. By using a property I can rest assured that the getter is synthesised for me, and so I don’t need to touch the implementation yet. In fact, I can’t do anything practical in the implementation because there is no class yet to represent the location service.

Note that we are NOT going to create a class yet to implement the location service – that is not necessary at the moment. In our AppDelegate.h we only need to add in:

@property (readonly) id locationService;

By using id as the type, we’ve put off the need to create a class. Any object will do.

Going back to our requirements, we see we have ‘gets notified’, so that seems to suggest that the AppDelegate will act as a delegate to the location service, and will therefore need to respond to a protocol.

Now we can start to bring our location service into being, but a little bit at a time. Lets add another expectation that AppDelegate responds to a location service delegate protocol.

it(@"should conform to LocationServiceDelegate", ^{

    [[sut should] conformToProtocol:@protocol(LocationServiceDelegate)];

});

We’ll need to add a header file to the project to contain the definition. I go with the convention of putting delegate protocols in with their respective class, so I’m going to create a header file (no need for a .m implementation yet) and call it LocationService.h.

@protocol LocationServiceDelegate < NSObject >

@end

Notice that I’ve no need at this stage to put any definitions into the protocol. I only need the protocol to exist to be able to add it to AppDelegate.

@interface AppDelegate : UIResponder < UIApplicationDelegate, LocationServiceDelegate >

Our test now passes, so we can move onto adding an expectation that the location service has some way of telling the AppDelegate about a new location.

it(@"should respond to locationService:didUpateLocations:", ^{

    [[sut should] respondToSelector:@selector(locationService:didUpdateLocations:)];

});

There is now an interface that we need to implement on the LocationServiceDelegate protocol.

@protocol LocationServiceDelegate < NSObject >

-(void)locationService: (id) locationService didUpdateLocations: (NSArray *) locations;

@end

Notice that again we are only using id when referring to the yet to be created LocationService class. Code that works in this situation, and to be honest will work later, although we might choose to refactor it so that we get better type safety and code completion assistance from Xcode.

AppDelegate now needs a stub implementation of the delegate callback.

-(void)locationService:(id)locationService didUpdateLocations:(NSArray *)locations {

}

That’s all we can do now to describe tests for the location service which still doesn’t exist, but we started to define how it will communicate with the AppDelegate. We can’t hook up the AppDelegate as a delegate to an object whose class doesn’t yet exist, so we’ll need to remember to come back here and check for that.

pending(@"is delegate of the locationService", ^{

});

Creating the Location Filter

I’m going to speed up a bit now, I don’t think you need a step by step tutorial on how to write unit tests, I’ll concentrate on when we introduce new elements of the design.

Our next set of requirements says that location provided by the location service should be passed to the location filter. The location filter is going to make sense out of the incoming CoreLocation data. Again the requirement suggests that AppDelegate is a delegate of the location filter, so we’ll need to check for the delegate protocol and that we are responding to it.

context(@"Passes new locations to the location filter", ^{

    it(@"should have a location filter", ^{

        [[sut should] respondToSelector:@selector(locationFilter)];

    });

    it(@"should call LocationFilter's addLocation:", ^{

        [[locationFilter should] receive:@selector(addLocation:)];

        [sut locationService: nil didUpdateLocations: @[ [CLLocation nullMock] ]];

    });
});




LocationFilter.h is created and looks like this:

@protocol LocationFilterDelegate < NSObject >

-(void)locationFilter: (id) locationFilter didAcceptLocation: (id) location;

@end

@protocol LocationFilter < NSObject >

@end

Notice that I’ve avoided specifying any specific class for the location. Whist this might well be CoreLocations CLLocation class, there is no need to create the dependency at the moment.

I’ve also take the opportunity to define the protocol that the LocationFilter class will use. Note that we do this with a @protocol, not an @interface. If we used an interface, we’d have to create an implementation as well. The protocol is sufficient for now.

Add a property to the AppDelegate interface to support the location filter:

@property (readonly) id locationFilter;

Because our requirement calls for locations to be passed to the location filter, we need an interface on the LocationFilter protocol to do that:

@protocol LocationFilter < NSObject >

-(void)addLocation: (id) location;

@end

And we need the AppDelegate to pass on the location to the filter when notified:

-(void)locationService:(id< LocationService >)locationService didUpdateLocations:(NSArray *)locations {

    [locations enumerateObjectsUsingBlock:^(id location, NSUInteger idx, BOOL *stop) {

        [self.locationFilter addLocation:location];

    }];

}

We should now have something that tests the integration the AppDelegate makes between the LocationService and LocationFilter, yet neither of those exist other than as protocols, and they are nicely separated, each independent of each other.

Time to move onto the next requirement, “Gets notified by the location filter when locations are accepted”. AppDelegate needs to be the delegate of the LocationFilter, although we can’t implement that at the moment as the LocationFilter still doesn’t exist. We can test that AppDelegate adopts the delegate protocol and responds to the delegate callback.

context(@"Gets notified by the location filter when locations are accepted", ^{

    pending(@"should be delegate to the location filter", ^{

    });

    it(@"should conform to LocationFilterDelegate protocol", ^{

        [[sut should] conformToProtocol:@protocol(LocationFilterDelegate)];

    });

    it(@"should respond to locationFilter:didAcceptLocation:", ^{

        [[sut should] respondToSelector:@selector(locationFilter:didAcceptLocation:)];

    });

});

AppDelegate.h gets amended:

@interface AppDelegate : UIResponder < UIApplicationDelegate, LocationServiceDelegate, LocationFilterDelegate >

As does AppDelegate.m:

-(void)locationFilter:(id)locationFilter didAcceptLocation:(id)location {

}

That seemed pretty simple!

Writing Locations to Persistent Storage

Now that we’ve got some sensible data to deal with, we should write it away somewhere for posterity (and so that we can send it to the server, sooner or later).

context(@"Writes accepted locations to storage", ^{

    it(@"should have a storage service", ^{

        [[sut should] respondToSelector:@selector(storageService)];

    });

    it(@"should call StorageService's addLocation:", ^{

        [[storageService should] receive:@selector(addLocation:)];

        [sut locationFilter:locationFilter didAcceptLocation:[CLLocation nullMock]];

    });

});

We need to add a property to AppDelegate.h for the storage service.

@property (readonly) id< StorageService > storageService;

And create StorageService.h so that we can define the protocol for the storage service.

@protocol StorageService < NSObject >

-(void)addLocation: (id) location;

@end

You might be wondering why I’m creating my own storage service when there are the likes of Core Data to deal with the task. Well, I can’t say I’ve decided one way or another at the moment, and there is no reason not to use Core Data in this case, but its non-trivial to setup and for the moment, not necessary. We are still trying to flesh out the outline of the applications responsibilities, so why get caught up in the detail at the moment? Also, I might well be included to put a front end on Core Data so that I can keep control of the services its expected to offer.

AppDelegate.m can now be updated to include the call to store the received location:

-(void)locationFilter:(id)locationFilter didAcceptLocation:(id)location {

    [self.storageService addLocation:location];

}

Upload Locations to the Server

Now that locations have been received, filtered and written to storage we need to a communications service to go about sending them to the server, as and when its able.

context(@"Notifies communications service that there are locations to upload", ^{

    it(@"should have a communications service", ^{

        [[sut should] respondToSelector:@selector(communicationService)];

    });

    it(@"should call CommunicationService's startUploading", ^{

        [[[communicationService should] receive] startUploading];

        [sut locationFilter:locationFilter didAcceptLocation:[CLLocation nullMock]];

    });

});

We need another object to represent the communications service, so we’ll amend AppDelegate.h to include a property for one.

@property (readonly) id communicationService;

And create ‘CommunicationService.h’ to include the protocol definition for the service.

@protocol CommunicationService 

-(void)startUploading;

@end

A simple change to AppDelegate.m to include a call to the comms service is all thats needed to complete the tests.

-(void)locationFilter:(id)locationFilter didAcceptLocation:(id)location {

[self.storageService addLocation:location];
[self.communicationService startUploading];

}

The Mocks Used to Test

One thing I’ve skipped over along the way is the creation of mocks for each of the services. You’ve seen references too them, but here is the beforeEach at the top of the spec that defines them:

beforeEach(^{

    sut = [[AppDelegate alloc] init];

    locationService = [KWMock nullMockForProtocol:@protocol(LocationService)];
    [[sut stubAndReturn:locationService] locationService];

    locationFilter = [KWMock nullMockForProtocol:@protocol(LocationFilter)];
    [[sut stubAndReturn:locationFilter] locationFilter];

    storageService = [KWMock nullMockForProtocol:@protocol(StorageService)];
    [[sut stubAndReturn:storageService] storageService];

    communicationService = [KWMock nullMockForProtocol:@protocol(CommunicationService)];
    [[sut stubAndReturn:communicationService] communicationService];

});

They are all fairly simple, a nullMock for each object, and stubbed into AppDelegate so that it doesn’t have to try and create real objects for the time being.

Conclusion

Well, that seems to be about it for now. We’ve met all of the outline requirements to define an app that collects, filters, stores and forwards location data. AppDelegate now has quite a good chunk of the code necessary to make this work in the real world. We can now tackle each of the interfaces that we’ve defined (LocationService, LocationFilter, etc.) Each of those needs its own spec, building upon the methodology that we’ve outlined here. As each is implemented, we’ll need to revisit the AppDelegateSpec and update it as the interfaces take shape (and update AppDelegate.m to include creation of the objects and setting the delegates).

I find that this really helps me to think clearly about the structure of the application, for its often too tempting to try and get stuck into working with the SDK. We often need to do some experimenting with the API’s in order to understand them, but by pushing areas of responsibility out into their own objects, we can create a safe and less muddied playing area for those experiments.

I hope that you can see that by using protocols and mocks in this way we can build out the structure of our app without needing to get too bogged down too early in the nitty gritty of the various system and third-party API’s that we’ll likely need in order to round this out into an app suitable for submission to the App Store.

Let me know if you have any queries or feedback on the approach in the comments.




Leave a Reply

Your email address will not be published. Required fields are marked *