xcode NSUserDefault 不应该是单元测试的白板吗?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/19084633/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-15 03:57:28  来源:igfitidea点击:

Shouldn't NSUserDefault be clean slate for unit tests?

xcodeunit-testing

提问by LisaD

I'm writing my first iOS unit tests (Xcode 5, iOS 6) and find that the results of the unit tests vary depending on what I've done in the Simulator lately. E.g. I click on a user in my contacts list in the Simulator, and now my "recent contacts" data in UserDefaults has one more object than before, even when I'm running unit tests.

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

For unit testing, it's not clean to have random user defaults data (I'm used to RoR tests with their own clean db). Besides, I might want to test specific states like having empty "recent contacts" data.

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

From looking at related questions here, I seem some possible answers that I'm not happy with.

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

  • Mock UserDefaults for the unit tests! I would have to modify many existing classes so that I can inject that mock.
  • Clear or customize UserDefaults in a setUp method! But then my data created laboriously in manual testing would be gone.
  • Clear or customize UserDefaults in a setUp method thenrestore those values in tearDown! Ouch.
  • 为单元测试模拟 UserDefaults!我将不得不修改许多现有的类,以便我可以注入该模拟。
  • 在 setUp 方法中清除或自定义 UserDefaults!但随后我在手动测试中费力创建的数据就会消失。
  • 在 setUp 方法中清除或自定义 UserDefaults,然后在 tearDown 中恢复这些值!哎哟。

These seem unnecessarily complicated for something that should be standard practice in unit tests. I don't want to repeat myself in every unit test. So, my questions are:

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

  • Am I missing something desirable about the way UserDefaults are persisted from ad-hoc Simulator testing through to unit test runs?
  • Is there a configurable way to fix this, say some way to set the unit test target to have different storage location for UserDefaults than when I use the Simulator to manually test?
  • Failing that, is there an elegant way to do this in code?
  • For example, I could have a MyAppTestCase object inherit from XCTestCase and override setUp and tearDown methods to always set aside then restore the UserDefaults. Is this a good idea?
  • 我是否遗漏了一些关于从临时模拟器测试到单元测试运行的用户默认值持久化方式的可取之处?
  • 有没有一种可配置的方法来解决这个问题,比如用某种方法将单元测试目标设置为与使用模拟器手动测试时不同的用户默认存储位置?
  • 如果做不到这一点,是否有一种优雅的方法可以在代码中做到这一点?
  • 例如,我可以让 MyAppTestCase 对象从 XCTestCase 继承并覆盖 setUp 和 tearDown 方法以始终留出然后恢复 UserDefaults。这是一个好主意吗?

回答by orkoden

Using named suites like in this answerworked well for me. Removing the user defaults used for testing could also be done in func tearDown().

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

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

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

回答by bigkm

Available iOS 7 / 10.9

可用 iOS 7 / 10.9

Rather than using the standardUserDefaults you can use a suite name to load your tests

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

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

This coupled with some code to remove the SomeOtherTests.plist file from the appropriate directory in setUpwill archive the desired result.

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

You would have to design any objects to take your defaults objects so that there wouldn't be any side effects from the tests.

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

回答by Rob Napier

As @Till suggests, your design is probably incorrect for good testability. Rather than having unit-testable pieces of the system read NSUserDefaultsdirectly, they should work with some other object (which may talk to NSUserDefaults). This is roughly equivalent to "mocking NSUserDefaults", but is really an extra abstraction layer. Your configuration object would abstract both NSUserDefaultsand other configuration storage like keychain. It would also ensure that you don't scatter string constants around the program. I've built this kind of config object for many projects and highly recommend it.

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

Some would argue that unit-testable objects shouldn't rely on singletons like NSUserDefaultsor my recommend global "configuration" object at all. Instead, all configuration should be injected at init. In practice, I find this to create too much headache when interacting with Storyboards, but it is worth considering in places where it can be useful.

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

If you really want to dig deeply into NSUserDefaults, it does provide some layering capability. You may investigate setVolatileDomain:forName:to see if you can create an extra layer for your unit test. In practice, I haven't had much luck with these kinds of things on iOS (more-so on Mac, but still not to the level you would need to trust it).

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

It is possible to swizzle standardUserDefaults, but I wouldn't recommend this approach if you can avoid it. Your "save everything at start and restore everything at end" is probably the best standardized way to approach the problem if you can't adapt your design to avoid externalities.

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

回答by sfoop

You can easily save & restore the persistent domain for the main bundle's identifier, which is what [[NSUserDefaults standardUserDefaults] setObject:forKey:]writes to. For example,

您可以轻松保存和恢复主包标识符的持久域,这是[[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]];

You can also use [[NSUserDefaults standardUserDefaults] volatileDomainForName:NSRegistrationDomain]if you want to access a single combined dictionary of the stuff you register using all the -registerDefaults:calls (at least for any code that has run up to where the unit test has started, of course).

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

回答by hfossli

I like to create a new one so there's no collision

我喜欢创建一个新的,所以没有冲突

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

回答by Olha Pavliuk

While I believe the Rob Napier's answeris the most reasonable, for those who needs just a quick-fix, here's my workaround:

虽然我相信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
    }
}

Cons:

缺点:

  1. works only with Stringkeys, unless you implement all needed types
  2. supports only runtime storage unless you dump it to some file.
  1. 仅适用于String键,除非您实现所有需要的类型
  2. 仅支持运行时存储,除非您将其转储到某个文件。

Pros:

优点:

  1. is agnostic of "Logic tests"/"Host application tests".
  2. works perfectly for run-time, so should work in a lifetime of a single test func.
  1. 与“逻辑测试”/“主机应用程序测试”无关。
  2. 非常适合运行时,所以应该在单个测试函数的生命周期内工作。