How to mock location service using KIF-framework
Asked Answered
F

2

6

I use KIF framework (http://github.com/kif-framework/KIF) for UI Tests and I need to mock location service.

The problem is location service starts BEFORE KIF method -beforeAll invoked. So it's too late to mock.

Any suggestions would be appreciated.

Farmer answered 21/3, 2014 at 14:2 Comment(1)
Can you provide any sample code to reproduce the issue ?Evelineevelinn
D
3

In my KIF target I have a BaseKIFSearchTestCase : KIFTestCase, where I overwrite CLLocationManager`s startUpdatingLocation in a category.

Note that this is the only category overwrite I ever made as this is really not a good idea in general. but in a test target I can accept it.

#import <CoreLocation/CoreLocation.h>

#ifdef TARGET_IPHONE_SIMULATOR


@interface CLLocationManager (Simulator)
@end

@implementation CLLocationManager (Simulator)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-protocol-method-implementation"

-(void)startUpdatingLocation 
{
    CLLocation *fakeLocation = [[CLLocation alloc] initWithLatitude:41.0096334 longitude:28.9651646];
    [self.delegate locationManager:self didUpdateLocations:@[fakeLocation]];
}
#pragma clang diagnostic pop

@end
#endif // TARGET_IPHONE_SIMULATOR



#import "BaseKIFSearchTestCase.h"

@interface BaseKIFSearchTestCase ()

@end

@implementation BaseKIFSearchTestCase
 //...

@end

Cleaner would be to have a subclass of CLLocationManager in your application target and another subclass with the same name in your test target that send fake location like shown above. But if this is possible depends on how your test target is set up, as it actually need to be an application target as Calabash uses it.


Yet another way:

  • in your project create another configuration "Testing", cloning "Debug"

  • add the Preprocessor Macro TESTING=1 to that configuration.

  • Subclass CLLocationManager

  • use that subclass where you would use CLLocaltionManger

  • conditionally compile that class

    #import "GELocationManager.h"
    
    @implementation GELocationManager
    -(void)startUpdatingLocation
    {
    
    #if TESTING==1
    #warning Testmode
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            CLLocation *fakeLocation = [[CLLocation alloc] initWithLatitude:41.0096334 longitude:28.9651646];
            [self.delegate locationManager:self didUpdateLocations:@[fakeLocation]];
        });
    
    #else
        [super startUpdatingLocation];
    #endif
    
    }
    @end
    
  • in your test targets scheme choose the new configuration


And yet another option:

enter image description here

Probably the best: no code needs to be changed.

Debacle answered 18/2, 2015 at 22:7 Comment(1)
Your third option, which seems so simple, doesn't seem to work for me. I choose San Francisco as my location, but when I run the tests I get the alert for a failed location call.Incongruity
R
1

As usual, a couple of ways to do this. The key is not to try to mock out the existing location service but to have a completely different mock you can get access to at run time. The first method I'm going to describe is basically building your own tiny DI container. The second method is for getting at singletons you don't normally have access to.

1) Refactor your code so that it doesn't use LocationService directly. Instead, encapsulate it in a holder (could be a simple singleton class). Then, make your holder test-aware. The way this is works is you have something like a LocationServiceHolder that has:

// Do some init for your self.realService and make this holder
// a real singleton.

+ (LocationService*) locationService {
  return useMock ? self.mockService : self.realService;
}

- (void)useMock:(BOOL)useMock {
  self.useMock = useMock;
}

- (void)setMock:(LocationService*)mockService {
  self.mockService = mockService;
}

Then whenever you need your locationService you call

[[LocationServiceHolder sharedService] locationService];  

So that when you're testing, you can do something like:

- (void)beforeAll {
  id mock = OCClassMock([LocationService class]);
  [[LocationServiceHolder sharedService] useMock:YES]];
  [[LocationServiceHolder sharedService] setMock:mock]];
}

- (void)afterAll {
  [[LocationServiceHolder sharedService] useMock:NO]];
  [[LocationServiceHolder sharedService] setMock:nil]];      
}

You can of course do this in beforeEach and rewrite the semantics to be a bit better than the base version I'm showing here.

2) If you are using a third party LocationService that's a singleton that you can't modify, it's slightly more tricky but still doable. The trick here is to use a category to override the existing singleton methods and expose the mock rather than the normal singleton. The trick within a trick is to be able to send the message back on to the original singleton if the mock doesn't exist.

So let's say you have a singleton called ThirdPartyService. Here's MockThirdPartyService.h:

static ThirdPartyService *mockThirdPartyService;

@interface ThirdPartyService (Testing)

+ (id)sharedInstance;
+ (void)setSharedInstance:(ThirdPartyService*)instance;
+ (id)mockInstance;

@end

And here is MockThirdPartyService.m:

#import "MockThirdPartyService.h"
#import "NSObject+SupersequentImplementation.h"

// Stubbing out ThirdPartyService singleton
@implementation ThirdPartyService (Testing)

+(id)sharedInstance {
    if ([self mockInstance] != nil) {
        return [self mockInstance];
    }
    // What the hell is going on here? See http://www.cocoawithlove.com/2008/03/supersequent-implementation.html
    IMP superSequentImp = [self getImplementationOf:_cmd after:impOfCallingMethod(self, _cmd)];
    id result = ((id(*)(id, SEL))superSequentImp)(self, _cmd);
    return result;
}

+ (void)setSharedInstance:(ThirdPartyService *)instance {
    mockThirdPartyService = instance;
}

+ (id)mockInstance {
    return mockThirdPartyService;
}

@end

To use, you would do something like:

#include "MockThirdPartyService.h"

...

id mock = OCClassMock([ThirdPartyService class]);
[ThirdPartyService setSharedInstance:mock];

// set up your mock and do your testing here

// Once you're done, clean up.
[ThirdPartyService setSharedInstance:nil];
// Now your singleton is no longer mocked and additional tests that
// don't depend on mock behavior can continue running.

See link for supersequent implementation details. Mad props to Matt Gallagher for the original idea. I can also send you the files if you need.

Conclusion: DI is a good thing. People complain about having to refactor and having to change your code just to test but testing is probably the most important part of quality software dev and DI + ApplicationContext makes things so much easier. We use Typhoon framework but even rolling your own and adopting the DI + ApplicationContext pattern is very much worth it if you're doing any level of testing.

Ribbentrop answered 18/2, 2015 at 22:35 Comment(10)
While your conclusion about code refactoring for test its correct you are not showing DI in your answer, as you switch internal behavior of the service and not pass in a test service object. DI and singletons never go well together.Debacle
@Debacle please reread my answer. I specifically say that only the first method is building your own tiny DI while the second is for mocking singletons you can't control, which I never claimed was DI. DI and singletons go great together. DI is what allows you to maintain any kind of scope for an object (singleton, weak singleton, etc) and still maintain testability.Ribbentrop
never heard the term "tiny DI" before. Must have missed that class.Debacle
@Debacle If I had answered "just use DI" I don't think that would have been helpful. And if the OP had DI in place this wouldn't be an issue. The question's context implied a lack of DI and awareness of DI so I gave one basic example of implementing a DI system without a lot of features but which gives you a minimum layer of abstraction. And a second for when things are beyond your control. Don't know where you are going with remarks like "must have missed that in class". Criticize my answer directly.Ribbentrop
ok, here is my critics: there is no reason why there should be a singleton involved at all. But exactly that is what your answer is implying. Why? I can instantiate CLLocationManger? I just can pass it where ever I need it. I don't need a singleton service around it that messes up my dependencies. I just need one service, that I pass to any object that needs it.Debacle
Even in DI the application context is generally a singleton (or at least long term life cycle object) to serve all dependency needs. For CLLocationManager specifically no, there is no need but the pattern I showed was generic and doesn't rely on CLLocationManager's semantics. The OP also didn't say he was using CLLocationManager specifically which is why I showed an example of "self.realService". Passing objects around is fine but having an application context that holds your dependencies allows you to patch your application context and avoid writing the category you showed in your answer.Ribbentrop
@Debacle I get that you're being a bit dogmatic about DI and singletons and that's fine. But my answer isn't for somebody who knows this stuff as the OP does not seem to and it also isn't meant to be perfect DI. My answer is trying to introduce OP to the idea of inversion of control, while at the same time not solving just a single case specific to CLLocationManager. I don't think your answer of using a category to override a single method is super flexible but it has its uses so I didn't go on a rant on your answer. As long as OP finds it useful. To each his own.Ribbentrop
I am not ranting your answer. you wanted critics, I provided. and I am not dogmatic. I just work in big teams and know the value of using healthy architecture. If a class uses singletons internally, it will create headache sooner or later. You are advertising DI and usage Singleton at the same post. for me that sounds super strange, as u say: "don't be lazy, refactor your code, but use singletons as anything else is too much work and dogmatic"Debacle
the only way singletons should be handled: self.endpointController = [[VSAPIEndpointController alloc] initWithUserDefaults:[NSUserDefaults standardUserDefaults] liveConfigController:self.liveConfigController];. For two reasons: in testing I can passing mocks of user defaults and any of my co-coders will know from the signature that the user defaults matters and have influence o this class.Debacle
Thanks for your answer. Some of it will be helpful, but I think it doesn't show how to solve original issue, where beforeAll is called after UIViewController is initialized and possibly already accessing CLLocationManager or something else.Ideo

© 2022 - 2024 — McMap. All rights reserved.