【问题标题】:How to mock NSDate in Swift?如何在 Swift 中模拟 NSDate?
【发布时间】:2015-04-27 07:04:40
【问题描述】:

我必须测试一些日期计算,但为此我需要在 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 始终是当前日期。

【问题讨论】:

  • 你为什么不简单地关闭你的系统自动日期和时间更新,改变你的计算机的日期并做你的测试
  • 我们在连接到 CI 解决方案的远程 mac 上运行测试。我不想太在意那台电脑。

标签: unit-testing swift mocking nsdate method-swizzling


【解决方案1】:

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

我发现this explanation 是一个巨大的帮助。

我必须在 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...)
    }
}

【讨论】:

  • 我最终得到了类似的解决方案,所以我会接受这个答案:)
【解决方案2】:

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

专门针对您的混乱问题,IIRC NSDate 是一个类集群,因此您要替换的方法不太可能被调用,因为将“静默”创建并返回不同的类。

【讨论】:

    【解决方案3】:

    如果你想调配它,你需要调配一个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)
    }
    

    以后你可以这样使用:

    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*) ()
    • 它不适用于 Swift 3 和 Date(),我不知道如何修复它
    • @ArkadiuszMatecki 无法使其与 Swift 中引入的 Date 一起使用,因为它是原生 Swift 类型,并且无法使用 swizzling。您需要将该方法与时钟类一起使用。
    • 这就是我的想法,但我找不到明确的答案。我最终创建了一个时钟类
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2014-08-02
    • 1970-01-01
    • 1970-01-01
    • 2016-10-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多