很明显,在UnitTest中,您
但是,如果您有一个全局结构,例如具有私人设置器且无法修改的游戏XP和游戏等级。应用启动时,我会从UserDefaults中自动加载此数据。无法更改输入时,如何测试访问该全局结构的方法?
示例:
import UIKit
//Global struct with private data
struct GameStatus {
private(set) static var xp: Int = 0
private(set) static var level: Int = 0
/// Holds all winning states
enum MyGameStatus {
case hasNotYetWon
case hasWon
}
/// Today's game state of the user against ISH
static var todaysGameStatus: MyGameStatus {
if xp >= 100 {
return .hasWon
} else {
return .hasNotYetWon
}
}
func restoreXpAndLevel() {
// reads UserData value
}
func increaseXp(for: Int) {
//...
}
}
// class with methods to test
class LevelView: UIView {
enum LevelState {
case showStart
case showCountdown
case showFinalCuontdown
}
var state: LevelState {
if GameStatus.xp > 95 {
return .showFinalCuontdown
} else if GameStatus.xp > 90 {
return .showCountdown
}
return .showStart
}
//...configurations depending on the level
}
答案 0 :(得分:2)
首先,LevelView看起来逻辑太多。视图的重点是显示模型数据。它不包括GameStatus.xp > 95
之类的业务逻辑。那应该在其他地方完成,并放入视图中。
接下来,为什么GameStatus是静态的?这只是使事情复杂化。更改时将GameStatus传递给视图。这就是视图控制器的工作。视图只是画东西。如果您认为视图中的任何内容都是真正可以单元测试的,则可能不应该在视图中显示。
最后,您要努力工作的部分是用户默认设置。因此,将其提取到通用GameStorage中。
protocol GameStorage {
var xp: Int { get set }
var level: Int { get set }
}
现在将UserDefaults设为GameStorage:
extension UserDefaults: GameStorage {
var xp: Int {
get { /* Read from UserDefaults */ return ... }
set { /* Write to UserDefaults */ }
}
var level: Int {
get { /* Read from UserDefaults */ return ... }
set { /* Write to UserDefaults */ }
}
}
要进行测试,请创建一个静态的:
struct StaticGameStorage: GameStorage {
var xp: Int
var level: Int
}
现在,当您创建GameStatus时,请传递其存储空间。但是您可以给它一个默认值,这样就不必一直传递它
class GameStatus {
private var storage: GameStorage
// A default parameter means you don't have to pass it normally, but you can
init(storage: GameStorage = UserDefaults.standard) {
self.storage = storage
}
这样,xp和level就可以传递到存储中。无需特殊的“立即加载存储”步骤。
private(set) var xp: Int {
get { return storage.xp }
set { storage.xp = newValue }
}
private(set) var level: Int {
get { return storage.level }
set { storage.level = newValue }
}
编辑:我在这里从GameStatus更改为类的结构。这是因为GameStatus缺少值语义。如果有两个GameStatus副本,并且您修改了其中一个,则另一个可能也会更改(因为它们都写入UserDefaults)。没有值语义的结构很危险。
可以重新获得值的语义,这值得考虑。例如,您可以回到原始设计,而不是通过xp和关卡传递到存储,该设计具有从存储加载的显式“恢复”步骤(并且我假设写入存储的“保存”步骤)。那么GameStatus将是合适的结构。
我还将提取LevelState,以便您可以更轻松地对其进行测试,并捕获视图之外的业务逻辑。
enum LevelState {
case showStart
case showCountdown
case showFinalCountDown
init(xp: Int) {
if xp > 95 {
self = .showFinalCountDown
} else if xp > 90 {
self = .showCountdown
}
self = .showStart
}
}
如果此视图仅使用过此方法,则可以嵌套它。只是不要将其设为私有。您可以测试LevelView.LevelState,而无需对LevelView本身做任何事情。
然后您可以根据需要更新视图的GameStatus:
class LevelView: UIView {
var gameStatus: GameStatus? {
didSet {
// Refresh the view with the new status
}
}
var state: LevelState {
guard let xp = gameStatus?.xp else { return .showStart }
return LevelState(xp: xp)
}
//...configurations depending on the level
}
现在视图本身不需要逻辑测试。您可以进行基于图像的测试,以确保在不同的输入下可以正确绘制图像,但这完全是端到端的。所有逻辑都很简单且可测试。您可以通过将StaticGameStorage传递给GameStatus来完全不使用UIKit来测试GameStatus和LevelState。
答案 1 :(得分:1)
解决方案是依赖注入!
您可以创建一个Persisting
协议和一个Facade类来与用户默认设置进行交互
protocol Persisting {
func getObject(key: String) -> Any?
func persist(value: Any, key: String)
}
final class Persist: Persisting {
func getObject(key: String) -> Any? {
return UserDefaults.standard.object(forKey: key)
}
func persist(object: Any, key: String) {
UserDefaults.standard.set(value: object, forKey: key)
}
}
class MockPersist: Persisting {
// this is set from the test
var mockObjectToReturn: Any?
func getObject(key: String) -> Any? {
return mockObjectToReturn
}
var didCallPersistObject: (Any?, String)
func persist(object: Any, key: String) {
didCallPersistObject.0 = object
didCallPersistObject.1 = key
}
}
现在在您的结构上,您需要向其注入类型为Persisting
的var。
在测试时,您需要注入MockPersist
并针对MockPersist
类中定义的var进行断言。
希望这会有所帮助