11

我必须测试一些日期计算,但为此我需要NSDate()在 Swift 中进行模拟。整个应用程序都是用 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始终是当前日期。

4

3 回答 3

9

免责声明——我是 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...)
    }
}
于 2015-12-04T18:32:09.127 回答
4

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

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

于 2015-04-27T07:59:11.747 回答
4

如果你想调配它,你需要调配一个内部使用的类,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或类似的东西,您可以传递并从中获取日期,并且您可以在测试中轻松地将其替换为替代实现。

于 2016-04-19T12:19:58.227 回答