1

所以我有一个我正在尝试对同时使用 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!
4

1 回答 1

3

我对交互的单元测试所做的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符合KeyValueStore. 不需要做任何其他事情,NSUbiquitousKeyValueStore因为它已经实现了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)
        }
    }

}

最后,我可以在我的单元测试中使用我的模拟,确保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)
    }

}
于 2020-07-19T18:54:27.970 回答