【问题标题】:How to mock NSUbiquitousKeyValueStore for unit testing?如何模拟 NSUbiquitousKeyValueStore 进行单元测试?
【发布时间】:2020-04-30 21:35:42
【问题描述】:

所以我有一个我正在尝试对同时使用 UserDefaults 和 NSUbiquitousKeyValueStore 的类进行单元测试。我可以使用 UserDefaults(suiteName: #file) 轻松模拟 UserDefaults(例如),但我不知道如何模拟 NSUbiquitousKeyValueStore。我似乎在 SO 上找不到任何关于它的帖子,而且我的 google-fu 也没有。

这是我的测试课的开头,仅供参考:

class ReviewTests: XCTestCase {

    private var userDefaults: UserDefaults = UserDefaults(suiteName: #file)!
    private var ubiquitousKeyValueStore: NSUbiquitousKeyValueStore = // How do I mock this?
    private var reviewPromptController: ReviewPromptController!

【问题讨论】:

    标签: ios swift xcode unit-testing nsubiquitouskeyvaluestore


    【解决方案1】:

    对我与NSUbiquitousKeyValueStore 的交互进行单元测试时,我所做的是创建一个协议,我的自定义云存储包装器围绕NSUbiquitousKeyValueStoreNSUbiquitousKeyValueStore 符合该协议。这些实现如下:

    protocol KeyValueStore: class {
    
        // MARK: Properties
        static var didChangeExternallyNotification: Notification.Name { get }
    
        // MARK: Reading
        func object(forKey aKey: String) -> Any?
        func string(forKey aKey: String) -> String?
        func array(forKey aKey: String) -> [Any]?
        func dictionary(forKey aKey: String) -> [String : Any]?
        func data(forKey aKey: String) -> Data?
        func longLong(forKey aKey: String) -> Int64
        func double(forKey aKey: String) -> Double
        func bool(forKey aKey: String) -> Bool
    
        // MARK: Writing
    
        func set(_ anObject: Any?, forKey aKey: String)
        func set(_ aString: String?, forKey aKey: String)
        func set(_ aData: Data?, forKey aKey: String)
        func set(_ anArray: [Any]?, forKey aKey: String)
        func set(_ aDictionary: [String : Any]?, forKey aKey: String)
        func set(_ value: Int64, forKey aKey: String)
        func set(_ value: Double, forKey aKey: String)
        func set(_ value: Bool, forKey aKey: String)
        func removeObject(forKey aKey: String)
    
    }
    

    KeyValueStore 是一个协议,其中包含NSUbiquitousKeyValueStore 具有的方法签名。通过这样做,我可以创建一个可以在可以使用NSUbiquitousKeyValueStore 实例的测试中使用的类型。

    接下来,我将NSUbiquitousKeyValueStore 符合KeyValueStoreNSUbiquitousKeyValueStore 不需要做任何其他事情,因为它已经实现了 KeyValueStore 中定义的方法。

    extension NSUbiquitousKeyValueStore: KeyValueStore { }
    

    然后,我创建了一个类型,它是NSUbiquitousKeyValueStore 的包装器。 UserSettingsStorage 接受任何符合 KeyValueStore 的类型。由于NSUbiquitousKeyValueStoreMockCloudKeyValueStore 都符合KeyValueStore,我可以在生产中传递NSUbiquitousKeyValueStore 的实例,在测试中传递MockCloudKeyValueStore 的实例。

    class MockCloudKeyValueStore: KeyValueStore {
    
        // MARK: Properties
    
        // KeyValueStore
        static var didChangeExternallyNotification: Notification.Name = Notification.Name(NSUbiquitousKeyValueStore.didChangeExternallyNotification.rawValue)
    
        // Helpers
        private var arrays: [String: [Any]] = [:]
        private var bools: [String: Bool] = [:]
        private var datas: [String: Data] = [:]
        private var dictionaries: [String: [String: Any]] = [:]
        private var doubles: [String: Double] = [:]
        private var longLongs: [String: Int64] = [:]
        private var objects: [String: Any] = [:]
        private var strings: [String: String] = [:]
    
        // MARK: Reading
        func object(forKey aKey: String) -> Any? {
            objects[aKey]
        }
    
        func string(forKey aKey: String) -> String? {
            strings[aKey]
        }
    
        func array(forKey aKey: String) -> [Any]? {
            arrays[aKey]
        }
    
        func dictionary(forKey aKey: String) -> [String : Any]? {
            dictionaries[aKey]
        }
    
        func data(forKey aKey: String) -> Data? {
            datas[aKey]
        }
    
        func longLong(forKey aKey: String) -> Int64 {
            longLongs[aKey] ?? 0
        }
    
        func double(forKey aKey: String) -> Double {
            doubles[aKey] ?? 0
        }
    
        func bool(forKey aKey: String) -> Bool {
            bools[aKey] ?? false
        }
    
        // MARK: Writing
    
        func set(_ anObject: Any?, forKey aKey: String) {
            objects[aKey] = anObject
            postServerDidChangeNotification()
        }
    
        func set(_ aString: String?, forKey aKey: String) {
            strings[aKey] = aString
            postServerDidChangeNotification()
        }
    
        func set(_ aData: Data?, forKey aKey: String) {
            datas[aKey] = aData
            postServerDidChangeNotification()
        }
    
        func set(_ anArray: [Any]?, forKey aKey: String) {
            arrays[aKey] = anArray
            postServerDidChangeNotification()
        }
    
        func set(_ aDictionary: [String : Any]?, forKey aKey: String) {
            dictionaries[aKey] = aDictionary
            postServerDidChangeNotification()
        }
    
        func set(_ value: Int64, forKey aKey: String) {
            longLongs[aKey] = value
            postServerDidChangeNotification()
        }
    
        func set(_ value: Double, forKey aKey: String) {
            doubles[aKey] = value
            postServerDidChangeNotification()
        }
    
        func set(_ value: Bool, forKey aKey: String) {
            bools[aKey] = value
            postServerDidChangeNotification()
        }
    
        func removeObject(forKey aKey: String) {
            arrays[aKey] = nil
            bools[aKey] = nil
            datas[aKey] = nil
            dictionaries[aKey] = nil
            doubles[aKey] = nil
            longLongs[aKey] = nil
            objects[aKey] = nil
            strings[aKey] = nil
            postServerDidChangeNotification()
        }
    
        // MARK: Helpers
    
        private func postServerDidChangeNotification() {
            var userInfo = [AnyHashable : Any]()
            let changeReason = NSUbiquitousKeyValueStoreServerChange
            userInfo[NSUbiquitousKeyValueStoreChangeReasonKey] = changeReason
            NotificationCenter.default.post(name: Self.didChangeExternallyNotification, object: nil, userInfo: userInfo)
        }
    
    }
    
    class UserSettingsStorage: ObservableObject {
    
        // MARK: Properties
    
        private let cloudKeyValueStore: KeyValueStore
    
        private let notificationCenter: NotificationCenter
    
        @Published var selectedListName = ""
    
        // MARK: Initialization
    
        init(cloudKeyValueStore: KeyValueStore,
             notificationCenter: NotificationCenter = .default) {
            self.cloudKeyValueStore = cloudKeyValueStore
            self.notificationCenter = notificationCenter
            observeUbiquitousKeyValueStoreDidChangeExternallyNotification()
        }
    
        // MARK: Reading
    
        private func getSelectedListIndex() {
            selectedListName = cloudKeyValueStore.string(forKey: "selectedListIndexKey") ?? ""
        }
    
        // MARK: Writing
    
        // Cloud Key-Value Store
    
        func setSelectedList(name: String) {
            selectedListName = name
            cloudKeyValueStore.set(name, forKey: "selectedListIndexKey")
        }
    
        // MARK: Notification Observation
    
        private func observeUbiquitousKeyValueStoreDidChangeExternallyNotification(in center: NotificationCenter = .default) {
            center.addObserver(self,
                           selector: #selector(handleUbiquitousKeyValueStoreDidChangeExternallyNotification(notification:)),
                           name: NSUbiquitousKeyValueStore.didChangeExternallyNotification,
                           object: nil)
        }
    
        // MARK: Notification Selectors
    
        @objc private func handleUbiquitousKeyValueStoreDidChangeExternallyNotification(notification: Notification) {
            // Check for the reasons listed at https://developer.apple.com/documentation/foundation/nsubiquitouskeyvaluestore/1433687-change_reason_values to handle errors/blockers
            if let userInfo = notification.userInfo, let changeReason = userInfo[NSUbiquitousKeyValueStoreChangeReasonKey] as? NSNumber {
                switch changeReason.intValue {
                case NSUbiquitousKeyValueStoreServerChange:
                    getSelectedListIndex()
                case NSUbiquitousKeyValueStoreInitialSyncChange:
                    print("initial sync change")
                case NSUbiquitousKeyValueStoreQuotaViolationChange:
                    print("quota violation change")
                case NSUbiquitousKeyValueStoreAccountChange:
                    print("account change")
                default:
                    print("unknown change")
                }
            } else {
                print("userInfo not available for Notification", notification)
            }
        }
    
    }
    

    最后,我可以在单元测试中使用我的 mock,确保从我的 mock 调用 NSUbiquiousKeyValueStore.didChangeExternallyNotification,然后验证我的 NSUbiquitousKeyValueStore 包装器是否按预期响应通知。

    class UnitTests: XCTestCase {
    
        // MARK: Properties
    
        var storage: UserSettingsStorage!
    
        // MARK: Lifecycle
    
        override func setUp() {
            super.setUp()
            storage = UserSettingsStorage(cloudKeyValueStore: MockCloudKeyValueStore())
        }
    
        override func tearDown() {
            storage = nil
            super.tearDown()
        }
    
        // MARK: Selected List Name
    
        func test_UserSettingsStorage_PublishesSelectedListName_WhenListNameIsSet() {
            let listName = "list name"
            let publishedExpectation = expectation(forNotification: NSUbiquitousKeyValueStore.didChangeExternallyNotification, object: nil) { notification in
                return self.storage.selectedListName == listName
            }
            storage.setSelectedList(name: listName)
            wait(for: [publishedExpectation], timeout: 2)
        }
    
    }
    

    【讨论】:

      猜你喜欢
      • 2018-04-04
      • 2021-09-27
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-08-19
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多