如何在 Swift 中模拟 NSDate?

How to mock NSDate in Swift?

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

我试过 method swizzling 但它不起作用(或者我做错了更有可能)。

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 始终是当前日期。

您应该真正设计您的系统以支持测试,而不是使用 swizzling。如果您进行大量数据处理,那么您应该将适当的日期注入到使用它的函数中。通过这种方式,您的测试将日期注入到这些函数中以测试它们,并且您还有其他测试可以验证在各种其他情况下是否会注入正确的日期(当您存根使用这些日期的方法时)。

特别针对您的混合问题,IIRC NSDate 是一个 class 集群,因此您要替换的方法不太可能被调用,因为另一个 class 将是 'silently' 创建并返回。

免责声明 -- 我是 Swift 测试的新手,所以这可能是一个非常糟糕的解决方案,但我也一直在努力解决这个问题,所以希望这能帮助到别人。

我发现 this explanation 帮了大忙。

我必须在 NSDate 和我的代码之间创建一个缓冲区 class:

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

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

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

然后在测试中创建一个继承自原始DateHandler()的mock,并"inject"将其放入待测代码中:

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

如果你想调整它,你需要调整 NSDate 内部使用的 class,它是 __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
}

正如其他人所建议的那样,我宁愿推荐引入一个 class Clock 或类似的东西,您可以传递它并从中获取日期,并且您可以在测试中轻松地将其替换为替代实现.