Warning: file_get_contents(/data/phpspider/zhask/data//catemap/2/unit-testing/4.json): failed to open stream: No such file or directory in /data/phpspider/zhask/libs/function.php on line 167

Warning: Invalid argument supplied for foreach() in /data/phpspider/zhask/libs/tag.function.php on line 1116

Notice: Undefined index: in /data/phpspider/zhask/libs/function.php on line 180

Warning: array_chunk() expects parameter 1 to be array, null given in /data/phpspider/zhask/libs/function.php on line 181
Xcode 不应该';对于单元测试,默认情况下不会有新的记录吗?_Xcode_Unit Testing - Fatal编程技术网

Xcode 不应该';对于单元测试,默认情况下不会有新的记录吗?

Xcode 不应该';对于单元测试,默认情况下不会有新的记录吗?,xcode,unit-testing,Xcode,Unit Testing,我正在编写我的第一个iOS单元测试(Xcode 5,iOS 6),发现单元测试的结果因我最近在模拟器中所做的工作而异。例如,我在模拟器中单击联系人列表中的某个用户,现在UserDefaults中的“最近联系人”数据比以前多了一个对象,即使在我运行单元测试时也是如此 对于单元测试,拥有随机的用户默认数据是不干净的(我习惯于使用自己干净的数据库进行RoR测试)。此外,我可能想测试一些特定的状态,比如有空的“最近的联系人”数据 从这里的相关问题来看,我似乎有一些我不满意的可能答案 单元测试的模拟用户

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

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

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

  • 单元测试的模拟用户默认值!我必须修改许多现有的类,以便能够注入该模拟
  • 清除或自定义设置方法中的用户默认值!但是,我在手工测试中费力创建的数据将不复存在
  • 在设置方法中清除或自定义用户默认值,然后在拆卸中恢复这些值!哎哟
对于单元测试中的标准实践来说,这些似乎是不必要的复杂。我不想在每个单元测试中重复我自己。因此,我的问题是:

  • 从即席模拟器测试到单元测试运行,用户默认值的持久化方式是否缺少一些可取之处
  • 有没有一种可配置的方法来解决这个问题,比如说,将单元测试目标设置为与使用模拟器手动测试时不同的UserDefaults存储位置
  • 如果做不到这一点,是否有一种优雅的方式在代码中实现这一点
  • 例如,我可以让一个MyAppTestCase对象从XCTestCase继承,并重写setUp和tearDown方法以始终保留,然后恢复UserDefaults。这是个好主意吗

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

有些人会争辩说,单元可测试对象根本不应该依赖于单一对象,比如
NSUserDefaults
或我推荐的全局“配置”对象。相反,所有配置都应该在init注入。在实践中,我发现在与故事板交互时,这会让人非常头疼,但在一些有用的地方,这是值得考虑的

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

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

iOS 7/10.9提供

您可以使用套件名称来加载测试,而不是使用standardUserDefaults

[[NSUserDefaults alloc] initWithSuiteName:@"SomeOtherTests"];
这与从
setUp
的相应目录中删除SomeOtherTests.plist文件的一些代码相结合,将归档所需的结果

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

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


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

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

我想创建一个新的,这样就不会发生碰撞

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"))
    }
}
虽然我认为这是最合理的,但对于那些只需要快速修复的人来说,以下是我的解决方法:

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
    }
}
缺点:

  • 仅适用于
    String
    键,除非实现了所有需要的类型
  • 仅支持运行时存储,除非将其转储到某个文件
  • 优点:

  • 不可知“逻辑测试”/“主机应用程序测试”
  • 在运行时可以完美地工作,因此应该在单个测试函数的生命周期内工作
    您不应该对用户默认值进行单元测试,因为这是您无法直接控制的。单元测试应该在原子软件单元本身上工作->不应该涉及其他类或服务。您的方法听起来更像是集成测试。后者通常使用模拟接口。我想你误解了我的问题。我如何在没有直接控制的情况下进行单元测试?我不打算用实际值引入用户默认值,另一个解决方案是使用像OCMock这样的隔离框架。将所谓的seam作为NSUserDefaults类型的属性添加到类中。测试中的类将使用存储在所述属性中的standardUserDefaults进行初始化。在测试中,您可以覆盖defau
    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
        }
    }