Faking CoreLocation... Inconceivable!

Matt Mongeau

I decided to get more familiar with integration testing iOS applications. In the past I’ve found trying to do integration testing analogous with climbing the side of a cliff.

''

Given that I’m no Dread Pirate Roberts this has proven very challenging in the past. To challenge myself even further I decided to write an integration test around CoreLocation. In the simulator I can set a location for myself and to some degree I could test with this, but I wanted to be able to have different test scenarios given different locations.

''

One solution was to use something like FTLocationSimulator and do the following:

#ifdef FAKE_CORE_LOCATION
    self.locationManager = [[FTLocationSimulator alloc] init];
#else
    self.locationManager = [[CLLocationManager alloc] init];
#endif

But I’m not a big fan of adding conditional flags in my code. Also I don’t feel like I have a lot of control over what happens in my code. Instead I prefer doing something closer to dependency injection. Wherever I would normally do [[CLLocationManager alloc] init] I instead do [MyLocationManager sharedInstance]. MyLocationManager is a singleton which will delegate methods to it’s own locationManager. The basic setup looks something like:

#import "MyLocationManager.h"
#import <CoreLocation/CoreLocation.h>

@implementation MyLocationManager
@synthesize locationManager;

static MyLocationManager *sharedSingleton;
+ (id)sharedInstance {
    static BOOL initialized = NO;
    if(!initialized) {
        initialized = YES;
        sharedSingleton = [[MyLocationManager alloc] init];
    }
    return sharedSingleton;
}
- (id) init {
    if (self = [super init]) {
        locationManager = [[CLLocationManager alloc] init];
    }
    return self;
}
@end

And my fake location manager:

#import "CLLocationManagerFake.h"
#import <CoreLocation/CoreLocation.h>

@implementation CLLocationManagerFake
@synthesize desiredAccuracy;
@synthesize location;

-(id) init {
    if (self = [super init]) {
        self->location = [[CLLocation alloc] initWithLatitude:0.0 longitude:0.0];
    }
    return self;
}

- (void)startUpdatingLocation {
    [self.delegate locationManager:self didUpdateToLocation:self->location fromLocation:self->location];
}

@end

And in my tests I can do the following:

CLLocationManagerFake *fake = [[CLLocationManagerFake alloc] init];
CLLocation *fakeLocation = [[CLLocation alloc] initWithLatitude:42.356426 longitude:-71.061993];
[fake setLocation:fakeLocation];
[[TTimeLocationManager sharedInstance] setLocationManager:fake];

Now I have complete control in my tests without polluting my actual code. If I want to use a different fake or have more control over specific scenarios I am able to do so with ease. I can also keep my test dependent code out of my final target.

''

“HE DIDN’T FALL? INCONCEIVABLE. ”

About thoughtbot

We've been helping engineering teams deliver exceptional products for over 20 years. Our designers, developers, and product managers work closely with teams to solve your toughest software challenges through collaborative design and development. Learn more about us.