You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

iOS开发:加载视图控制器前Mock UserDefaults的问题

我懂你在iOS测试里Mock UserDefaults时遇到的糟心事——直接继承UserDefaults确实容易踩坑,毕竟它是单例设计,还会和真实的持久化数据纠缠不清。先聊聊你当前实现里的潜在问题,再给你两个更靠谱的解决方案。

问题分析

你当前的MockUserDefaults直接继承了UserDefaults,这里有两个核心问题:

  • 调用super.init(suiteName:)还是会关联到真实的系统存储域,哪怕你调用了removePersistentDomain,也可能存在数据残留或者测试并发时的干扰
  • 你定义的FakeData字典还没真正接管UserDefaults的读写逻辑,测试中如果调用了未重写的方法(比如object(forKey:)),还是会访问真实的存储,达不到Mock的目的
解决方案1:用协议抽象(推荐)

这种方式遵循依赖倒置原则,把UserDefaults的行为抽象成协议,然后分别实现真实版本和Mock版本,彻底隔离测试与真实存储。

第一步:定义协议

先把你业务中用到的UserDefaults方法抽象出来:

protocol UserDefaultsProtocol {
    func object(forKey defaultName: String) -> Any?
    func set(_ value: Any?, forKey defaultName: String)
    func removeObject(forKey defaultName: String)
    // 按需添加其他常用方法,比如integer(forKey:)、bool(forKey:)等
}

第二步:让真实UserDefaults遵循协议

给系统的UserDefaults做个扩展,自动遵循协议:

extension UserDefaults: UserDefaultsProtocol {}

第三步:实现Mock类

这个Mock类完全基于内存字典,和真实存储毫无关联:

class MockUserDefaults: UserDefaultsProtocol {
    private var storage: [String: Any?] = [:]
    
    func object(forKey defaultName: String) -> Any? {
        storage[defaultName] ?? nil
    }
    
    func set(_ value: Any?, forKey defaultName: String) {
        storage[defaultName] = value
    }
    
    func removeObject(forKey defaultName: String) {
        storage.removeValue(forKey: defaultName)
    }
    
    // 示例:添加bool类型的方法
    func bool(forKey defaultName: String) -> Bool {
        storage[defaultName] as? Bool ?? false
    }
}

第四步:在业务代码中注入协议

修改你的业务类,通过初始化方法注入UserDefaultsProtocol,默认使用真实的UserDefaults.standard:

class UserProfileService {
    private let userDefaults: UserDefaultsProtocol
    
    init(userDefaults: UserDefaultsProtocol = UserDefaults.standard) {
        self.userDefaults = userDefaults
    }
    
    func saveNickname(_ nickname: String) {
        userDefaults.set(nickname, forKey: "user_nickname")
    }
    
    func getNickname() -> String? {
        userDefaults.object(forKey: "user_nickname") as? String
    }
}

第五步:编写测试用例

测试时直接传入MockUserDefaults,完全隔离真实存储:

import XCTest
@testable import YourApp

class UserProfileServiceTests: XCTestCase {
    func testSaveAndGetNickname() {
        let mockDefaults = MockUserDefaults()
        let service = UserProfileService(userDefaults: mockDefaults)
        
        // 执行保存操作
        service.saveNickname("iOSDev")
        
        // 验证Mock中的数据
        XCTAssertEqual(mockDefaults.object(forKey: "user_nickname") as? String, "iOSDev")
        XCTAssertEqual(service.getNickname(), "iOSDev")
    }
}
解决方案2:重写UserDefaults方法(不推荐但可用)

如果你坚持要继承UserDefaults,必须重写所有业务中用到的读写方法,避免访问真实存储:

class MockUserDefaults: UserDefaults {
    typealias FakeData = [String: Any?]
    var data: FakeData = [:]
    
    // 用唯一UUID作为suiteName,避免和其他测试/真实App冲突
    convenience init() {
        self.init(suiteName: UUID().uuidString)!
    }
    
    override init?(suiteName suitename: String?) {
        data = [:]
        super.init(suiteName: suitename)
        // 清空这个suite的持久化数据,防止残留
        self.removePersistentDomain(forName: suitename!)
    }
    
    // 重写set方法,只操作自己的data字典
    override func set(_ value: Any?, forKey defaultName: String) {
        data[defaultName] = value
    }
    
    // 重写object(forKey:)方法,返回自己data中的数据
    override func object(forKey defaultName: String) -> Any? {
        data[defaultName] ?? nil
    }
    
    // 按需重写其他方法,比如removeObject
    override func removeObject(forKey defaultName: String) {
        data.removeValue(forKey: defaultName)
    }
}

注意:这种方式必须确保所有用到的UserDefaults方法都被重写,否则会调用父类的真实实现,导致测试污染。

总结

优先推荐协议抽象的方式,它更灵活,测试隔离更彻底,也符合现代iOS开发的最佳实践。继承UserDefaults的方式虽然可行,但容易因为遗漏重写方法而引入隐藏问题。

内容的提问来源于stack exchange,提问作者Kilom94l

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.27 03:53:35