如何在Swift中模拟NSDate?

Mar*_*ski 9 unit-testing mocking nsdate swift method-swizzling

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

我试过方法调整但它不起作用(或者我做错了什么更有可能).

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

测试:

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()
// ...
}
Run Code Online (Sandbox Code Playgroud)

date总是当前的日期.

lon*_*tle 8

免责声明 - 我是Swift测试的新手,所以这可能是一个可怕的hacky解决方案,但我也一直在努力解决这个问题,所以希望这会帮助别人.

我发现这个解释是一个巨大的帮助.

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

class DateHandler {
   func currentDate() -> NSDate! {
      return NSDate()
   }
}
Run Code Online (Sandbox Code Playgroud)

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

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

然后在测试中创建一个继承自原始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...)
    }
}
Run Code Online (Sandbox Code Playgroud)


Tom*_*art 6

如果你想调配它,你需要调配一个由 内部使用的类,NSDate它是__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)
}
Run Code Online (Sandbox Code Playgroud)

稍后你可以这样使用:

let date = NSDate(timeIntervalSince1970: 946684800) // 2000-01-01
timeTravel(to: date) { 
    print(NSDate()) // 2000-01-01
}
Run Code Online (Sandbox Code Playgroud)

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

  • @ArkadiuszMatecki 无法使其与 Swift 中引入的“Date”一起使用,因为它是原生 Swift 类型,并且 swizzling 无法使用它。您需要将该方法与时钟类一起使用。 (4认同)

Wai*_*ain 5

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

特别是对于您的混合问题,IIRCNSDate是一个类集群,因此您要替换的方法不太可能被调用,因为将“静默”创建并返回不同的类。