我使用 KIF 框架 ( http://github.com/kif-framework/KIF ) 进行 UI 测试,我需要模拟位置服务。
问题是位置服务在 KIF 方法 -beforeAll 调用之前启动。所以现在嘲笑为时已晚。
任何建议,将不胜感激。
我使用 KIF 框架 ( http://github.com/kif-framework/KIF ) 进行 UI 测试,我需要模拟位置服务。
问题是位置服务在 KIF 方法 -beforeAll 调用之前启动。所以现在嘲笑为时已晚。
任何建议,将不胜感激。
在我的 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 使用它的应用程序目标。
还有一种方式:
在您的项目中创建另一个配置“测试”,克隆“调试”
添加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
在您的测试目标方案中选择新配置
还有另一种选择:
可能是最好的:不需要更改任何代码。
像往常一样,有几种方法可以做到这一点。关键不是要尝试模拟现有的位置服务,而是要有一个完全不同的模拟,您可以在运行时访问。我要描述的第一种方法基本上是构建您自己的微型 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 模式也是非常值得的。