如何模拟和测试存储字符串的 UserDefaults computed 属性?

How to mock and test a UserDefaults computed property that store String?

我正在尝试模拟 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)
    }
  }
}

您可以继承 UserDefaults :

(source)

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。在 production/staging 中你可以有 UserDefaults.standard 并且为了测试你可以有 MockUserDefaults

#if TESTING
    let userDefaults: UserDefaults = UserDefaults.standard
    #else
    let userDefaults: UserDefaults = MockUserDefaults(suiteName: "testing") ?? UserDefaults.standard
#endif

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

然后你创建一个符合该协议并使用 UserDefaults

的实际 class

然后您可以用 class 实例化您的 UserDefaultsService

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

上面的内容可能有点冗长,所以让我们分解一下。

注意上面的"static"部分我也去掉了,好像没必要,省了省事,希望没问题

1。创建协议

这应该是你感兴趣的全部

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

2。创建实际 Class

此 class 将与 UserDefaults 一起使用,但它 "hidden" 落后于协议。

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。用 Class

实例化 UserDefaultsService

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

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

UserDefaultsService 不知道 - 也不关心 - SettingsContainer 实际对值做了什么,只要它可以给出和接受 token,那么 UserDefaultsService很开心。

这是它的样子,请注意我们正在传递一个默认参数,所以我们甚至不必传递 SettingsContainer 除非我们必须这样做。

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 测试

"Finally"你说:)

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

MockSettingsContainer
class MockSettingsContainer: SettingsContainer {
    var token: String?
}

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

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

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

最后的笔记

以上内容可能看起来工作量很大,但重要的是要理解:

  • 将第 3 方组件(如 UserDefaults)包装在协议后面,以便您以后可以根据需要自由更改它们(例如在测试时)。
  • 在您的 classes 中使用这些协议而不是 "real" classes 的依赖项,这样您 - 再次 - 可以自由更改 classes .只要符合协议就万事大吉:)

希望对您有所帮助。

一个很好的解决方案是不要费心创建模拟或提取协议。而是像这样在您的测试中初始化一个 UserDefaults 对象:

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

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

Brief article here