像往常一样,有几种方法可以做到这一点。关键不是要尝试模拟现有的位置服务,而是要有一个完全不同的模拟,您可以在运行时访问。我要描述的第一种方法基本上是构建您自己的微型 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 模式也是非常值得的。