35

我正在编写我的第一个 iOS 单元测试(Xcode 5、iOS 6),并发现单元测试的结果取决于我最近在模拟器中所做的事情。例如,我在模拟器中单击我的联系人列表中的一个用户,现在我在 UserDefaults 中的“最近联系人”数据比以前多了一个对象,即使我正在运行单元测试也是如此。

对于单元测试,拥有随机用户默认数据并不干净(我习惯于使用他们自己的干净数据库进行 RoR 测试)。此外,我可能想测试特定状态,例如有空的“最近联系人”数据。

通过查看此处的相关问题,我似乎有一些我不满意的可能答案。

  • 模拟单元测试的 UserDefaults !我将不得不修改许多现有的类,以便可以注入该模拟。
  • 在 setUp 方法中清除或自定义 UserDefaults!但随后我在手动测试中费力创建的数据就会消失。
  • 在 setUp 方法中清除或自定义 UserDefaults,然后在 tearDown 中恢复这些值!哎哟。

对于应该成为单元测试标准实践的东西来说,这些似乎不必要地复杂。我不想在每个单元测试中重复自己。所以,我的问题是:

  • 关于从临时模拟器测试到单元测试运行的 UserDefaults 持久化方式,我是否遗漏了一些可取的东西?
  • 有没有一种可配置的方法来解决这个问题,比如将单元测试目标设置为具有与使用模拟器手动测试时不同的 UserDefaults 存储位置的方法?
  • 如果做不到这一点,有没有一种优雅的方法可以在代码中做到这一点?
  • 例如,我可以让 MyAppTestCase 对象继承自 XCTestCase 并覆盖 setUp 和 tearDown 方法以始终搁置然后恢复 UserDefaults。这是一个好主意吗?
4

6 回答 6

56

在这个答案中使用命名套件对我来说效果很好。删除用于测试的用户默认值也可以在func tearDown().

class MyTest : XCTestCase {
    var userDefaults: UserDefaults?
    let userDefaultsSuiteName = "TestDefaults"

    override func setUp() {
        super.setUp()
        UserDefaults().removePersistentDomain(forName: userDefaultsSuiteName)
        userDefaults = UserDefaults(suiteName: userDefaultsSuiteName)
    }
}
于 2016-12-14T17:05:00.093 回答
23

适用于 iOS 7 / 10.9

您可以使用套件名称来加载测试,而不是使用标准用户默认值

[[NSUserDefaults alloc] initWithSuiteName:@"SomeOtherTests"];

这与一些代码相结合,从适当的目录中删除 SomeOtherTests.plist 文件setUp将存档所需的结果。

您必须设计任何对象来获取默认对象,以便测试不会产生任何副作用。

于 2014-06-26T11:24:19.110 回答
19

正如@Till 建议的那样,您的设计可能不适合良好的可测试性。而不是NSUserDefaults直接读取系统的可单元测试部分,它们应该与其他一些对象(可能与 对话NSUserDefaults)一起工作。这大致相当于“模拟NSUserDefaults”,但实际上是一个额外的抽象层。您的配置对象将抽象两者NSUserDefaults和其他配置存储,如钥匙串。它还可以确保您不会在程序周围散布字符串常量。我已经为许多项目构建了这种配置对象并强烈推荐它。

有些人会争辩说,可单元测试的对象根本不应该依赖像NSUserDefaults我推荐的全局“配置”对象这样的单例。相反,所有配置都应该在 init 中注入。在实践中,我发现这在与 Storyboard 交互时会让人头疼,但在可能有用的地方值得考虑。

如果你真的想深入挖掘NSUserDefaults,它确实提供了一些分层功能。您可以调查setVolatileDomain:forName:一下是否可以为您的单元测试创​​建一个额外的层。在实践中,我在 iOS 上的这些事情上运气不佳(在 Mac 上更是如此,但仍然没有达到你需要信任它的程度)。

可以 swizzle standardUserDefaults,但如果可以避免,我不推荐这种方法。如果您无法调整设计以避免外部性,那么您的“开始时保存所有内容并最终恢复所有内容”可能是解决问题的最佳标准化方法。

于 2013-09-30T00:04:40.730 回答
5

我喜欢创建一个新的,所以没有碰撞

import XCTest

extension UserDefaults {
    private static var index = 0
    static func createCleanForTest(label: StaticString = #file) -> UserDefaults {
        index += 1
        let suiteName = "UnitTest-UserDefaults-\(label)-\(index)"
        UserDefaults().removePersistentDomain(forName: suiteName)
        return UserDefaults(suiteName: suiteName)!
    }
}

class MyTest: XCTestCase {

    func testOne() {
        let userDefaults = UserDefaults.createCleanForTest()
        XCTAssertFalse(userDefaults.bool(forKey: "foo"))
        userDefaults.set(true, forKey: "foo")
        XCTAssertTrue(userDefaults.bool(forKey: "foo"))
    }

    func testTwo() {
        let userDefaults = UserDefaults.createCleanForTest()
        XCTAssertFalse(userDefaults.bool(forKey: "foo"))
        userDefaults.set(true, forKey: "foo")
        XCTAssertTrue(userDefaults.bool(forKey: "foo"))
    }
}
于 2017-06-13T13:11:41.533 回答
4

您可以轻松地保存和恢复主包标识符的持久域,这是[[NSUserDefaults standardUserDefaults] setObject:forKey:]写入的内容。例如,

NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSDictionary *originalValues = [defaults persistentDomainForName:[[NSBundle mainBundle] bundleIdentifier]];

// do stuff, possibly [defaults removePersistentDomainForName:[[NSBundle mainBundle] bundleIdentifier]]
// or using setPersistentDomain: to substitute a dictionary of mock values and test against that

[defaults setPersistentDomain:originalValues forName:[[NSBundle mainBundle] bundleIdentifier]];

[[NSUserDefaults standardUserDefaults] volatileDomainForName:NSRegistrationDomain]如果您想使用所有调用访问您注册的内容的单个组合字典,您也可以使用-registerDefaults:(当然,至少对于已经运行到单元测试开始位置的任何代码)。

于 2016-12-29T22:20:50.310 回答
1

虽然我相信Rob Napier 的回答是最合理的,但对于那些只需要快速修复的人来说,这是我的解决方法:

class MockUserDefaults: UserDefaults {
    private var dict: [String: Any?] = [:]
    override func set(_ value: Any?, forKey defaultName: String) {
        dict[defaultName] = value
    }
    override func value(forKey key: String) -> Any? {
        return dict[key] ?? nil
    }
}

缺点:

  1. 仅适用于String键,除非您实现所有需要的类型
  2. 仅支持运行时存储,除非您将其转储到某个文件。

优点:

  1. 与“逻辑测试”/“主机应用程序测试”无关。
  2. 非常适合运行时工作,因此应该在单个测试函数的生命周期内工作。
于 2020-05-22T13:29:02.800 回答