1

我正在尝试模拟 UserDefaults 以测试其行为。

在不破坏保存用户令牌密钥的真实计算属性的情况下,最好的方法是什么?

class UserDefaultsService {

  private struct Keys {
    static let token = "partageTokenKey"
  }

  //MARK: - Save or retrieve the user token
  static var token: String? {
    get {
      return UserDefaults.standard.string(
        forKey: Keys.token)
    }
    set {
      UserDefaults.standard.set(
        newValue, forKey: Keys.token)
    }
  }
}
4

3 回答 3

3

您可以子类化UserDefaults

来源

class MockUserDefaults : UserDefaults {

  convenience init() {
    self.init(suiteName: "Mock User Defaults")!
  }

  override init?(suiteName suitename: String?) {
    UserDefaults().removePersistentDomain(forName: suitename!)
    super.init(suiteName: suitename)
  }

}

UserDefaultsService不是直接访问UserDefaults.standard,您可以根据您正在运行的目标创建属性。在生产/登台中,您可以拥有UserDefaults.standard,而在测试中,您可以拥有MockUserDefaults

您应该在使用它们之前添加 PREPROCESSOR 标志

#if TESTING
    let userDefaults: UserDefaults = UserDefaults.standard
    #else
    let userDefaults: UserDefaults = MockUserDefaults(suiteName: "testing") ?? UserDefaults.standard
#endif
于 2019-08-06T08:36:06.140 回答
2

一种方法是将您UserDefaults的协议包装在协议中并公开您需要的内容。

然后创建一个符合该协议并使用的实际类UserDefaults

然后,您可以UserDefaultsService使用该类实例化您的。

当您需要测试时,您可以创建一个符合相同协议的模拟并使用它来代替。这样你就不会“污染”你的UserDefaults.

上面的内容可能看起来有点拗口,所以让我们分解一下。

请注意,在上面我也删除了“静态”部分,这似乎没有必要,没有它会更容易,希望没关系

1. 创建协议

这应该是您有兴趣公开的所有内容

protocol SettingsContainer {
    var token: String? { get set }
}

2.创建一个实际的类

此类将与协议一起使用,UserDefaults但它“隐藏”在协议后面。

class UserDefaultsContainer {
    private struct Keys {
        static let token = "partageTokenKey"
    }
}

extension UserDefaultsContainer: SettingsContainer {
    var token: String? {
        get {
            return UserDefaults.standard.string(forKey: Keys.token)
        }
        set {
            UserDefaults.standard.set(newValue, forKey: Keys.token)
        }
    }
}

3. 使用该类实例化 UserDefaultsService

现在我们创建一个你的实例,UserDefaultsService它有一个符合SettingsContainer协议的对象。

美妙之处在于您可以稍后更改提供的类......例如在测试时。

TheUserDefaultsService不知道 - 也不关心 - 无论SettingsContainer实际对 value 做了什么,只要它可以给予和接受 a token,那么 theUserDefaultsService就是快乐的。

看起来是这样的,注意我们正在传递一个默认参数,所以我们甚至不必传递 aSettingsContainer除非我们必须。

class UserDefaultsService {
    private var settingsContainer: SettingsContainer

    init(settingsContainer: SettingsContainer = UserDefaultsContainer()) {
        self.settingsContainer = settingsContainer
    }

    var token: String? {
        get {
            return settingsContainer.token
        }
        set {
            settingsContainer.token = newValue
        }
    }
}

您现在可以UserDefaultsService像这样使用新的:

let userDefaultsService = UserDefaultsService()
print("token: \(userDefaultsService.token)")

4 测试

“终于”你说:)

要测试上述内容,您可以创建一个MockSettingsContainer符合SettingsContainer

class MockSettingsContainer: SettingsContainer {
    var token: String?
}

并将其传递给UserDefaultsService测试目标中的新实例。

let mockSettingsContainer = MockSettingsContainer()
let userDefaultsService = UserDefaultsService(settingsContainer: mockSettingsContainer)

现在可以测试您是否UserDefaultsService可以实际保存和检索数据而不会造成污染UserDefaults

最后的笔记

上面的内容可能看起来很多工作,但要理解的重要一点是:

  • 将 3rd 方组件(如UserDefaults)包装在协议后面,以便您以后可以根据需要(例如在测试时)随意更改它们。
  • 在您的类中使用这些协议而不是“真实”类的依赖项,这样您 - 再次 - 可以自由更改类。只要它们符合协议,一切都很好:)

希望有帮助。

于 2019-08-06T08:43:59.930 回答
1

一个非常好的解决方案是不用费心创建模拟或提取协议。而是在您的测试中初始化一个 UserDefaults 对象,如下所示:

let userDefaults = UserDefaults(suiteName: #file)
userDefaults.removePersistentDomain(forName: #file)

现在您可以继续使用您已经在扩展中定义的 UserDefaults 键,甚至可以根据需要将其注入任何功能!凉爽的。这将防止您的实际 UserDefaults 被触摸。

简短的文章在这里

于 2021-03-09T01:20:08.947 回答