如何模拟和测试存储字符串的 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 被触及。
我正在尝试模拟 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 实例化您的 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 被触及。