Stubbing NSDate to return a mock date can easily be done using category except for -[NSDate init]. -[NSDate init] is not called unlike other methods. class_addMethod does not help. method_exchangeImplementations, method_setImplementation on -[NSDate init] actually change -[NSObject init] but no effect on -[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);
The output is
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
has to important characteristics:In such a case,
+alloc
returns only a placeholder and you send-init…
to that placeholder (of class__NSPlaceholderDate
). Replacing-init
(NSDate
) has no effect, if-init
(__NSPlaceholderDate
or NSWhatever is implemented.)This is, because
+alloc
cannot decide which (private) subclass to choose, because it has no parameters. (They are passed at the-init…
.)You can
-init
of__NSPlaceholderDate
-init
of what+alloc
returns.+alloc
to return your private placeholder and overwrite-init
in it.