Stub -[NSDate init]

527 views Asked by At

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
2

There are 2 answers

2
Amin Negm-Awad On BEST ANSWER

NSDate has to important characteristics:

  • It is a class cluster
  • It is immutable

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

  • simply replace -init of __NSPlaceholderDate
  • replace -init of what +alloc returns.
  • replace +alloc to return your private placeholder and overwrite -init in it.
0
tsnorri On

If you need mock dates e.g. in your tests, consider instantiating the NSDate objects with the Factory pattern and replacing the factory for production or tests. This way only your own classes end up with the mock dates and you don't have to worry about accidentally replacing methods that may be used by Apple's frameworks.