【问题标题】:How to mock location service using KIF-framework如何使用 KIF 框架模拟位置服务
【发布时间】:2014-04-28 22:50:57
【问题描述】:

我使用 KIF 框架 (http://github.com/kif-framework/KIF) 进行 UI 测试 我需要模拟位置服务。

问题是定位服务在 KIF 方法 -beforeAll 调用之前启动。 所以现在嘲笑为时已晚。

任何建议将不胜感激。

【问题讨论】:

  • 您能否提供任何示例代码来重现该问题?

标签: ios objective-c automated-tests ui-automation kif


【解决方案1】:

在我的 KIF 目标中,我有一个 BaseKIFSearchTestCase : KIFTestCase,我在其中覆盖了一个类别中的 CLLocationManager 的 startUpdatingLocation。

请注意,这是我做过的唯一一个类别覆盖,因为这通常不是一个好主意。但在测试目标中我可以接受。

#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 将在您的应用程序目标中拥有一个CLLocationManager 的子类,并在您的测试目标中拥有另一个具有相同名称的子类,如上所示发送假位置。但是这是否可能取决于您的测试目标是如何设置的,因为它实际上需要成为 Calabash 使用它的应用程序目标。


另一种方式:

  • 在您的项目中创建另一个配置“Testing”,克隆“Debug”

  • Preprocessor Macro TESTING=1 添加到该配置中。

  • 子类CLLocationManager

  • 在您将使用 CLLocaltionManger 的地方使用该子类

  • 有条件地编译那个类

    #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
    
  • 在您的测试目标方案中选择新配置


还有另一种选择:

可能是最好的:无需更改代码。

【讨论】:

  • 您的第三个选项看起来很简单,但似乎对我不起作用。我选择旧金山作为我的位置,但是当我运行测试时,我收到了位置呼叫失败的警报。
【解决方案2】:

像往常一样,有几种方法可以做到这一点。关键不是要尝试模拟现有的位置服务,而是要有一个完全不同的模拟,您可以在运行时访问。我要描述的第一种方法基本上是构建您自己的微型 DI 容器。第二种方法是获取您通常无法访问的单身人士。

1) 重构您的代码,使其不直接使用 LocationService。相反,将其封装在一个持有者中(可以是一个简单的单例类)。然后,让您的持有人具有测试意识。这样做的方式是你有一个类似 LocationServiceHolder 的东西:

// 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;
}

然后,当你需要你的 locationService 时,你调用

[[LocationServiceHolder sharedService] locationService];  

所以当你在测试的时候,你可以做这样的事情:

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

您当然可以在 beforeEach 中执行此操作,并将语义重写为比我在此处显示的基本版本更好。

2) 如果您使用的第三方 LocationService 是您无法修改的单例,它会稍微复杂一些,但仍然可行。这里的技巧是使用一个类别来覆盖现有的单例方法并公开模拟而不是普通的单例。技巧中的技巧是,如果模拟不存在,则能够将消息发送回原始单例。

假设您有一个名为 ThirdPartyService 的单例。这是 MockThirdPartyService.h:

static ThirdPartyService *mockThirdPartyService;

@interface ThirdPartyService (Testing)

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

@end

这里是 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

要使用,您可以执行以下操作:

#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.

有关后续实施详情,请参阅链接。疯狂道具马特加拉格尔的原始想法。如果您需要,我也可以将文件发送给您。

结论:DI是个好东西。人们抱怨必须重构和更改代码只是为了测试,但测试可能是高质量软件开发中最重要的部分,而 DI + ApplicationContext 使事情变得更加容易。我们使用 Typhoon 框架,但如果您要进行任何级别的测试,即使滚动您自己的框架并采用 DI + ApplicationContext 模式也是非常值得的。

【讨论】:

  • 当您关于代码重构以测试其正确性的结论时,您没有在答案中显示 DI,因为您切换了服务的内部行为并且没有传入测试服务对象。 DI 和单例永远不会很好地结合在一起。
  • @vikingosegundo 请重新阅读我的回答。我特别说,只有第一种方法是构建您自己的微型 DI,而第二种方法是用于模拟您无法控制的单例,我从未声称这是 DI。 DI 和单例一起很好。 DI 允许您维护对象的任何类型的范围(单例、弱单例等)并仍然保持可测试性。
  • 以前从未听说过“微型 DI”这个词。一定错过了那节课。
  • @vikingosegundo 如果我回答“只使用 DI”,我认为这不会有帮助。如果 OP 有 DI,这将不是问题。问题的上下文暗示缺乏 DI 和对 DI 的认识,因此我给出了一个基本示例,说明了实现一个没有很多功能但为您提供最低抽象层的 DI 系统。当事情超出您的控制范围时,还有一秒钟。不知道您要去哪里,并带有诸如“一定在课堂上错过了”之类的言论。直接批评我的回答。
  • 好的,这是我的批评者:根本没有理由涉及单身人士。但这正是您的答案所暗示的。为什么?我可以实例化CLLocationManger?我可以将它传递到我需要它的任何地方。我不需要一个会打乱我的依赖的单例服务。我只需要一项服务,我将它传递给任何需要它的对象。
猜你喜欢
  • 2021-12-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-04-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多