ios 如何在Swift中测试和模拟属性 Package 器?

yk9xbfzb  于 2022-12-05  发布在  iOS
关注(0)|答案(2)|浏览(138)

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

@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
    }
}

我现在声明一个对象,该对象将保存存储在UserDefaults中的所有值。

struct UserDefaultsStorage {
    @DefaultsStorage(key: "userName")
    var userName: String?
}

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

final class ViewModel {
    func getUserName() -> String? {
        UserDefaultsStorage().userName
    }
}

这里几乎没有问题。
1.在这种情况下,我似乎不得不使用.standard用户默认值。如何使用UserDefaults的其他/模拟示例测试该视图模型?
1.如何使用UserDefaults的其他/模拟示例测试该属性 Package 器?我是否必须创建一个新类型,它是上述DefaultsStorage的干净副本,传递模拟的UserDefaults并测试该对象?

struct TestUserDefaultsStorage {
    @DefaultsStorage(key: "userName", storage: UserDefaults(suiteName: #file)!)
    var userName: String?
}
rpppsulh

rpppsulh1#

正如@mat在注解中提到的,你需要一个protocol来模拟UserDefaults依赖。

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

extension UserDefaults: UserDefaultsStorage {}

然后,您可以更改DefaultsStorage属性Wrapper以使用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
    }
}

之后,模拟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
    }
}

要测试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")
    }
}
oxalkeyp

oxalkeyp2#

这可能不是最好的解决方案,但是,我还没有找到一种方法来将使用属性 Package 器的UserDefaults注入到ViewModel中。如果有这样的选择,那么gcharita使用另一种协议的建议将是一个很好的实现。
我在测试类中使用了与ViewModel中相同的UserDefaults。我在每次测试之前保存原始值,并在每次测试之后恢复它们。

class ViewModelTests: XCTestCase {
    
    private lazy var userDefaults = newUserDefaults()
    private var preTestsInitialValues: PreTestsInitialValues!

    override func setUpWithError() throws {
        savePreTestUserDefaults()
    }
    
    override func tearDownWithError() throws {
        restoreUserDefaults()
    }
    
    private func newUserDefaults() -> UserDefaults.Type {
        return UserDefaults.self
    }
    
    private func savePreTestUserDefaults() {
        preTestsInitialValues = PreTestsInitialValues(userName: userDefaults.userName)
    }
    
    private func restoreUserDefaults() {
        userDefaults.userName = preTestsInitialValues.userName
    }

    func testUsername() throws {
       //"inject" User Defaults with the desired values
        let username = "No one"
        userDefaults.userName = username
        
        let viewModel = ViewModel()
        let usernameFromViewModel = viewModel.getUserName()
        XCTAssertEqual(username, usernameFromViewModel)
    }
}

struct PreTestsInitialValues {
    let userName: String?
}

相关问题