Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to mock NSDate in Swift?

I have to test some date calculation but to do so I need to mock NSDate() in Swift. Whole app is written in Swift and I'd like to write test in it as well.

I've tried method swizzling but it doesn't work (or I'm doing something wrong which is more likely).

extension NSDate {
    func dateStub() -> NSDate {
        println("swizzzzzle")
        return NSDate(timeIntervalSince1970: 1429886412) // 24/04/2015 14:40:12
    }
}

test:

func testCase() {
    let original = class_getInstanceMethod(NSDate.self.dynamicType, "init")
    let swizzled = class_getInstanceMethod(NSDate.self.dynamicType, "dateStub")
    method_exchangeImplementations(original, swizzled)
    let date = NSDate()
// ...
}

but date is always current date.

like image 440
Marcin Zbijowski Avatar asked Apr 27 '15 07:04

Marcin Zbijowski


2 Answers

If you want to swizzle it you need to swizzle a class that is internally used by NSDate and it is __NSPlaceholderDate. Use this only for testing since it is a private API.

func timeTravel(to date: NSDate, block: () -> Void) {
    let customDateBlock: @convention(block) (AnyObject) -> NSDate = { _ in date }
    let implementation = imp_implementationWithBlock(unsafeBitCast(customDateBlock, AnyObject.self))
    let method = class_getInstanceMethod(NSClassFromString("__NSPlaceholderDate"), #selector(NSObject.init))
    let oldImplementation = method_getImplementation(method)
    method_setImplementation(method, implementation)
    block()
    method_setImplementation(method, oldImplementation)
}

And later you can use like this:

let date = NSDate(timeIntervalSince1970: 946684800) // 2000-01-01
timeTravel(to: date) { 
    print(NSDate()) // 2000-01-01
}

As others suggested I would rather recommend introducing a class Clock or similar that you can pass around and get a date from it and you can easily replace it with an alternative implementation in your tests.

like image 142
Tomáš Linhart Avatar answered Oct 18 '22 19:10

Tomáš Linhart


Rather than use swizzling you should really design your system to support testing. If you do a lot of data processing then you should inject the appropriate date into the functions which use it. In this way your test injects the dates into these functions to test them and you have other tests which verify that the correct dates will be injected (when you stub the methods that use the dates) for various other situations.

Specifically for your swizzling problem, IIRC NSDate is a class cluster so the method you're replacing is unlikely to be called as a different class will be 'silently' created and returned.

like image 40
Wain Avatar answered Oct 18 '22 20:10

Wain