如何在Swift中模拟NSDate?

13

我需要测试一些日期计算,但是为了这样做,我需要在Swift中模拟NSDate()。整个应用程序都是用Swift编写的,我也想在其中编写测试。

我尝试过方法交换,但它不起作用(或者更可能是我做错了什么)。

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

测试:

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()
// ...
}

但是date始终为当前日期。


为什么不直接关闭系统自动日期和时间更新,更改计算机的日期并进行测试呢? - Leo Dabus
6
我们在连接到CI解决方案的远程Mac上运行测试。我不想过多干扰那台电脑。 - Marcin Zbijowski
3个回答

10

免责声明--我对Swift测试很陌生,所以这可能是一个非常笨拙的解决方案,但我也一直在努力解决这个问题,希望这能帮助到某些人。

我发现这个解释对我有很大的帮助。

我不得不在NSDate和我的代码之间创建一个缓冲类:

class DateHandler {
   func currentDate() -> NSDate! {
      return NSDate()
   }
}

然后在使用NSDate()的任何代码中使用缓冲区类,并将默认的DateHandler()作为可选参数提供。

class UsesADate {
   func fiveSecsFromNow(dateHandler: DateHandler = DateHandler()) -> NSDate! {
      return dateHandler.currentDate().dateByAddingTimeInterval(5)
   }
}

在测试中创建一个继承自原始DateHandler()的模拟对象,并将其“注入”到要测试的代码中:

class programModelTests: XCTestCase {

    override func setUp() {
        super.setUp()

        class MockDateHandler:DateHandler {
            var mockedDate:NSDate! =    // whatever date you want to mock

            override func currentDate() -> NSDate! {
                return mockedDate
            }
        }
    }

    override func tearDown() {
        super.tearDown()
    }

    func testAddFiveSeconds() {
        let mockDateHandler = MockDateHandler()
        let newUsesADate = UsesADate()
        let resultToTest = usesADate.fiveSecondsFromNow(dateHandler: mockDateHandler)
        XCTAssertEqual(resultToTest, etc...)
    }
}

我最终得到了类似的解决方案,所以我会接受这个答案 :) - Marcin Zbijowski

6

如果你想进行swizzle操作,你需要对NSDate内部使用的类进行swizzle,即__NSPlaceholderDate。由于这是一个私有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)
}

之后,您可以像这样使用:

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

正如其他人建议的,我更愿意推荐引入一个类Clock或类似的东西,你可以传递它并从中获取日期,你可以轻松地在测试中替换它为另一个实现。


在32位模拟器上崩溃 #1 0x006ded32 in (anonymous namespace)::AutoreleasePoolPage::pop(void*) () - kas-kad
它无法与Swift 3和Date()一起使用,我无法弄清楚如何修复它。 - Arkadiusz Matecki
4
无法使用 Swift 中引入的 Date 类型使其工作,因为它是原生的 Swift 类型,而 swizzling 无法与其配合使用。您需要使用时钟类的方法。 - Tomáš Linhart
这就是我所想的,但是我找不到确定的答案。最终我创建了一个时钟类。 - Arkadiusz Matecki

5
与其使用交换方法技术,你应该真正地设计你的系统来支持测试。如果你进行大量数据处理,则应将适当的日期注入到使用它的函数中。通过这种方式,你的测试向这些函数注入日期以进行测试,并且你还有其他测试来验证在各种其他情况下(当你存根使用日期的方法时)将 注入正确的日期。
对于你的交换方法问题,特别是针对 NSDate,我记得它是一个类集群,因此很可能不会调用你要替换的方法,而是会'静默'创建并返回一个不同的类。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接