【问题标题】:Stub -[NSDate init]存根 -[NSDate 初始化]
【发布时间】:2014-11-26 14:58:23
【问题描述】:

使用除 -[NSDate init] 之外的类别可以轻松地为 NSDate 存根以返回模拟日期。 -[NSDate init] 不像其他方法那样被调用。 class_addMethod 没有帮助。 method_exchangeImplementations, method_setImplementation on -[NSDate init] 实际上改变了 -[NSObject init] 但对 -[NSDate init] 没有影响。

[NSDate setMockDate:[NSDate dateWithTimeIntervalSinceReferenceDate:0]];
NSDate *date1 = [NSDate date];
NSLog(@"%@", date1);
NSLog(@"%.0f", [date1 timeIntervalSinceNow]);

// _replacement_Method is not called!
NSDate *date2 = [[NSDate alloc] init];
NSLog(@"%@", date2);
NSLog(@"%.0f", [date2 timeIntervalSinceNow]);

// _replacement_Method is called
NSObject *object = [[NSObject alloc] init];
NSLog(@"%@", object);

// A class with empty implementation to test inherited init from NSObject
// _replacement_Method is called by -[MyObject init]
MyObject *myobject = [[MyObject alloc] init];
NSLog(@"%@", myobject);

输出是

2001-01-01 00:00:00 +0000
-0
2014-11-26 14:43:26 +0000
438705806
<NSObject: 0x7fbc50e19d90>
<MyObject: 0x7fbc50e4ad30>

NSDate+Mock.m

#import "NSDate+Mock.h"

#import <mach/clock.h>
#import <mach/mach.h>
#import <objc/runtime.h>

static NSTimeInterval sTimeOffset;
static IMP __original_Method_Imp;

id _replacement_Method(id self, SEL _cmd)
{
    return ((id(*)(id,SEL))__original_Method_Imp)(self, _cmd);
}

@implementation NSDate (Mock)

+ (NSObject *)lock
{
    static dispatch_once_t onceToken;
    static NSObject *lock;
    dispatch_once(&onceToken, ^{
        lock = [[NSObject alloc] init];
    });
    return lock;
}

+ (void)setMockDate:(NSDate *)date
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Method m1 = class_getInstanceMethod([NSDate class], @selector(init));
        Method m2 = class_getInstanceMethod([NSDate class], @selector(initMock));
//        method_exchangeImplementations(m1, m2);
//        class_addMethod([NSDate class], @selector(init), (IMP)_replacement_Method, "@@:");
        __original_Method_Imp = method_setImplementation(m1, (IMP)_replacement_Method);
    });

    @synchronized([self lock]) {
        sTimeOffset = [date timeIntervalSinceReferenceDate] - [self trueTimeIntervalSinceReferenceDate];
    }
}

+ (NSTimeInterval)mockTimeOffset
{
    @synchronized([self lock]) {
        return sTimeOffset;
    }
}

+ (NSTimeInterval)trueTimeIntervalSinceReferenceDate
{
    clock_serv_t cclock;
    mach_timespec_t mts;
    host_get_clock_service(mach_host_self(), CALENDAR_CLOCK, &cclock);
    clock_get_time(cclock, &mts);
    mach_port_deallocate(mach_task_self(), cclock);
    NSTimeInterval now = mts.tv_sec + mts.tv_nsec * 1e-9 - NSTimeIntervalSince1970;
    return now;
}

+ (NSTimeInterval)timeIntervalSinceReferenceDate
{
    return [self trueTimeIntervalSinceReferenceDate] + [self mockTimeOffset];
}

+ (instancetype)date
{
    return [[NSDate alloc] initWithTimeIntervalSinceNow:0];
}

+ (instancetype)dateWithTimeIntervalSinceNow:(NSTimeInterval)secs
{
    return [[NSDate alloc] initWithTimeIntervalSinceNow:secs];
}

//- (instancetype)init
//{
//    self = [super init];
//    return self;
//}

//- (instancetype)initMock
//{
//    self = nil;
//    NSDate *date = [[NSDate alloc] initWithTimeIntervalSinceNow:0];
//    return date;
//}

- (instancetype)initWithTimeIntervalSinceNow:(NSTimeInterval)secs
{
    return [self initWithTimeIntervalSinceReferenceDate:[NSDate timeIntervalSinceReferenceDate] + secs];
}

- (NSTimeInterval)timeIntervalSinceNow
{
    NSTimeInterval t = [self timeIntervalSinceReferenceDate];
    return t - [NSDate timeIntervalSinceReferenceDate];
}

@end

【问题讨论】:

  • 你不能在你的代码中使用工厂方法(例如[NSDate date])吗?
  • 并非代码的每一部分都使用 [NSDate date]

标签: objective-c mocking stub objective-c-category method-swizzling


【解决方案1】:

NSDate具有重要特征:

  • 是类簇
  • 它是不可变的

在这种情况下,+alloc 仅返回一个占位符,您将 -init… 发送给该占位符(__NSPlaceholderDate 类)。如果 -init__NSPlaceholderDate 或 NSWhatever 已实现)替换 -init (NSDate) 无效。

这是因为+alloc 无法决定选择哪个(私有)子类,因为它没有参数。 (它们通过-init… 传递。)

你可以

  • 只需替换-init__NSPlaceholderDate
  • 替换 -init+alloc 返回的内容。
  • 替换 +alloc 以返回您的私有占位符并覆盖其中的 -init

【讨论】:

  • 如何准确替换 NSPlaceholderDate 的 init ?像这样 ? class_getInstanceMethod(NSClassFromString(@"__NSPlaceholderDate"), @selector(init));
  • 这得到一个实例方法。寻找class_replaceMethod()
【解决方案2】:

如果您需要模拟日期,例如在您的测试中,考虑用Factory pattern 实例化NSDate 对象并替换工厂以进行生产或测试。这样,只有您自己的类以模拟日期结束,您不必担心意外替换 Apple 框架可能使用的方法。

【讨论】:

    猜你喜欢
    • 2012-05-29
    • 2015-01-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多