如何在 Swift 中测试和模拟属性包装器?

gas*_*sho 5 ios swift property-wrapper

假设我有一个使用 的属性包装器的非常常见的用例UserDefaults

@propertyWrapper
struct DefaultsStorage<Value> {
    private let key: String
    private let storage: UserDefaults
    
    var wrappedValue: Value? {
        get {
            guard let value = storage.value(forKey: key) as? Value else {
                return nil
            }
            
            return value
        }
        
        nonmutating set {
            storage.setValue(newValue, forKey: key)
        }
    }
    
    init(key: String, storage: UserDefaults = .standard) {
        self.key = key
        self.storage = storage
    }
}
Run Code Online (Sandbox Code Playgroud)

我现在声明一个对象,该对象将保存存储在UserDefaults.

struct UserDefaultsStorage {
    @DefaultsStorage(key: "userName")
    var userName: String?
}
Run Code Online (Sandbox Code Playgroud)

现在,当我想在某个地方使用它时,比如说在视图模型中,我会有这样的东西。

final class ViewModel {
    func getUserName() -> String? {
        UserDefaultsStorage().userName
    }
}
Run Code Online (Sandbox Code Playgroud)

这里几乎没有什么问题。

  1. 在这种情况下,我似乎不得不使用.standard用户默认值。如何使用其他/模拟实例来测试该视图模型UserDefaults
  2. 如何使用其他/模拟实例来测试该属性包装器UserDefaults?我是否必须创建一个新类型,它是上述类型的干净副本DefaultsStorage,通过模拟UserDefaults并测试该对象?
struct TestUserDefaultsStorage {
    @DefaultsStorage(key: "userName", storage: UserDefaults(suiteName: #file)!)
    var userName: String?
}
Run Code Online (Sandbox Code Playgroud)

gch*_*ita 4

正如 @mat 在评论中已经提到的,您需要一个protocol模拟UserDefaults依赖项。像这样的事情会做:

protocol UserDefaultsStorage {
    func value(forKey key: String) -> Any?
    func setValue(_ value: Any?, forKey key: String)
}

extension UserDefaults: UserDefaultsStorage {}
Run Code Online (Sandbox Code Playgroud)

然后您可以更改DefaultsStoragepropertyWrapper 以使用UserDefaultsStorage引用而不是UserDefaults

@propertyWrapper
struct DefaultsStorage<Value> {
    private let key: String
    private let storage: UserDefaultsStorage

    var wrappedValue: Value? {
        get {
            return storage.value(forKey: key) as? Value
        }
        nonmutating set {
            storage.setValue(newValue, forKey: key)
        }
    }

    init(key: String, storage: UserDefaultsStorage = UserDefaults.standard) {
        self.key = key
        self.storage = storage
    }
}
Run Code Online (Sandbox Code Playgroud)

之后模拟UserDefaultsStorage可能如下所示:

class UserDefaultsStorageMock: UserDefaultsStorage {
    var values: [String: Any]

    init(values: [String: Any] = [:]) {
        self.values = values
    }

    func value(forKey key: String) -> Any? {
        return values[key]
    }

    func setValue(_ value: Any?, forKey key: String) {
        values[key] = value
    }
}
Run Code Online (Sandbox Code Playgroud)

为了测试DefaultsStorage,传递一个实例UserDefaultsStorageMock作为其存储参数:

import XCTest

class DefaultsStorageTests: XCTestCase {
    class TestUserDefaultsStorage {
        @DefaultsStorage(
            key: "userName",
            storage: UserDefaultsStorageMock(values: ["userName": "TestUsername"])
        )
        var userName: String?
    }
    
    func test_userName() {
        let testUserDefaultsStorage = TestUserDefaultsStorage()
        
        XCTAssertEqual(testUserDefaultsStorage.userName, "TestUsername")
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 您好,感谢您的回答。这完全有道理,但我相信这并不能回答我的第一个问题。您将如何测试在不使用“.standard”用户默认值的情况下使用此存储的视图模型? (3认同)