NSUserDefault不应该是单元测试的清单吗?

时间:2013-09-29 23:51:18

标签: xcode unit-testing

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

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

从这里查看相关问题,我似乎有些可能的答案,我不满意。

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

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

  • 我是否遗漏了一些关于UserDefaults从ad-hoc模拟器测试持续到单元测试运行的方式?
  • 是否有一种可配置的方法来解决这个问题,比如某种方式将单元测试目标设置为具有不同的UserDefaults存储位置,而不是使用模拟器进行手动测试?
  • 如果失败了,有没有一种优雅的方法在代码中执行此操作?
  • 例如,我可以从XCTestCase继承MyAppTestCase对象并覆盖setUp和tearDown方法,以便始终保留然后恢复UserDefaults。这是个好主意吗?

6 个答案:

答案 0 :(得分:37)

使用命名套件like in this answer对我来说效果很好。删除用于测试的用户默认值也可以在func tearDown()中完成。

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

    override func setUp() {
        super.setUp()
        UserDefaults().removePersistentDomain(forName: userDefaultsSuiteName)
        userDefaults = UserDefaults(suiteName: userDefaultsSuiteName)
    }
}

答案 1 :(得分:21)

可用iOS 7 / 10.9

您可以使用套件名称加载测试

,而不是使用standardUserDefaults
[[NSUserDefaults alloc] initWithSuiteName:@"SomeOtherTests"];

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

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

答案 2 :(得分:15)

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

有些人认为单元可测试对象不应该依赖于NSUserDefaults之类的单例或者我推荐的全局“配置”对象。相反,所有配置都应该在init注入。在实践中,我发现在与Storyboard交互时会产生太多的麻烦,但是在有用的地方值得考虑。

如果您真的想深入研究NSUserDefaults,它确实提供了一些分层功能。您可以调查setVolatileDomain:forName:以查看是否可以为单元测试创​​建额外的图层。在实践中,我对iOS上的这些东西并没有太多的好运(更多的是在Mac上,但仍然没有达到你需要信任它的水平)。

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

答案 3 :(得分:2)

您可以轻松保存&恢复主包标识符的持久域,这是# app/Resources/views/default/index.html.twig {% extends 'base.html.twig' %} {% block body %} {% for review in reviews %} {{ review.comment ~ '-' ~ review.book.name }} {% endfor %} {% endblock %} 写入的内容。例如,

[[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](至少对于已经运行到单元测试的任何代码)当然已经开始了。

答案 4 :(得分:2)

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

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"))
    }
}

答案 5 :(得分:0)

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

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. 在运行时完美运行,因此应在单个测试功能的生命周期内正常工作。