Как тестировать и моделировать оболочки свойств в Swift?

#ios #swift #property-wrapper

#iOS #swift #свойство-оболочка

Вопрос:

Допустим, у меня есть очень распространенный вариант использования оболочки свойств 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
    }
}
 

Сейчас я объявляю объект, в котором будут храниться все мои значения UserDefaults .

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

Теперь, когда я хочу использовать его где-нибудь, скажем, в модели представления, у меня было бы что-то вроде этого.

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

Здесь возникает несколько вопросов.

  1. Похоже, в этом случае я обязан использовать .standard пользовательские значения по умолчанию. Как протестировать эту модель представления, используя другой / смоделированный экземпляр UserDefaults ?
  2. Как протестировать эту оболочку свойств, используя другой / mocked экземпляр UserDefaults ? Должен ли я создавать новый тип, который является чистой копией вышеуказанного DefaultsStorage , передавать mocked UserDefaults и тестировать этот объект?
 struct TestUserDefaultsStorage {
    @DefaultsStorage(key: "userName", storage: UserDefaults(suiteName: #file)!)
    var userName: String?
}
 

Комментарии:

1. Обычный ключ к издевательству — это протокол…

2. Вы имеете в виду издевательство над всем UserDefaultsStorage ?

3. Я имею в виду издевательство над ошибками пользователя.

4. Извините, но это именно то, чего я не могу достичь.

5. Но вы не хотите издеваться над ошибками пользователя?

Ответ №1:

Как @mat уже упоминалось в комментариях, вам нужна protocol UserDefaults зависимость от макета. Что-то вроде этого подойдет:

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

extension UserDefaults: UserDefaultsStorage {}
 

Затем вы можете изменить свой DefaultsStorage propertyWrapper, чтобы использовать 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")
    }
}
 

Комментарии:

1. Привет и спасибо за ответ. Это полностью имеет смысл, однако я считаю, что это не отвечает на мой первый вопрос. Как бы вы протестировали модель представления, в которой это хранилище используется без использования .standard пользовательских настроек по умолчанию?

2. @gasho в моем примере я тестирую DefaultsStorage . Чтобы ваша модель представления была тестируемой, вы должны ввести новый протокол, который DefaultsStorage должен соответствовать. Ваша модель представления должна иметь ссылку на этот протокол, а DefaultsStorage не напрямую, и тогда вы сможете издеваться DefaultsStorage и вводить свой макет экземпляра в свою модель представления. В общем, когда вы хотите что-то протестировать, вы должны иметь возможность имитировать все его зависимости.