在Swift中模拟singleton / sharedInstance

时间:2016-01-29 21:54:56

标签: swift mocking singleton xctest

我有一个我想用XCTest测试的类,这个类看起来像这样:

public class MyClass: NSObject {
    func method() {
         // Do something...
         // ...
         SingletonClass.sharedInstance.callMethod()
    }
}

该类使用以此形式实现的单例:

public class SingletonClass: NSObject {

    // Only accessible using singleton
    static let sharedInstance = SingletonClass()

    private override init() {
        super.init()
    }

    func callMethod() {
        // Do something crazy that shouldn't run under tests
    }
}

现在进行测试。我想测试method()实际上做了它应该做的事情,但我不想调用callMethod()中的代码(因为它做了一些可怕的异步/网络/线程的东西,不应该在MyClass的测试下运行,并且会使测试崩溃)。

所以我基本上想做的是:

SingletonClass = MockSingletonClass: SingletonClass {
    override func callMethod() {
        // Do nothing
    }
let myObject = MyClass()
myObject.method()
// Check if tests passed

这显然不是有效的Swift,但你明白了。如何才能为此特定测试覆盖callMethod(),使其无害?

编辑:我尝试使用依赖注入的方式解决这个问题,但遇到了大问题。我创建了一个自定义的init方法,仅用于测试,以便我可以像这样创建我的对象:

let myObject = MyClass(singleton: MockSingletonClass)

让MyClass看起来像这样

public class MyClass: NSObject {
    let singleton: SingletonClass

    init(mockSingleton: SingletonClass){
        self.singleton = mockSingleton
    }

    init() {
        singleton = SingletonClass.sharedInstance
    }

    func method() {
         // Do something...
         // ...
         singleton.callMethod()
    }
}

将测试代码与其余代码混合是我觉得有点不愉快,但还可以。最大的问题是我在我的项目中有两个像这样构建的单身人士,彼此引用:

public class FirstSingletonClass: NSObject {
    // Only accessible using singleton
    static let sharedInstance = FirstSingletonClass()

    let secondSingleton: SecondSingletonClass

    init(mockSingleton: SecondSingletonClass){
        self.secondSingleton = mockSingleton
    }

    private override init() {
        secondSingleton = SecondSingletonClass.sharedInstance
        super.init()
    }

    func someMethod(){
        // Use secondSingleton
    }
}

public class SecondSingletonClass: NSObject {
    // Only accessible using singleton
    static let sharedInstance = SecondSingletonClass()

    let firstSingleton: FirstSingletonClass

    init(mockSingleton: FirstSingletonClass){
        self.firstSingleton = mockSingleton
    }

    private override init() {
        firstSingleton = FirstSingletonClass.sharedInstance
        super.init()
    }

    func someOtherMethod(){
        // Use firstSingleton
    }
}

当首次使用的单例之一,init方法等待另一个的init方法时,这会产生死锁,等等...

3 个答案:

答案 0 :(得分:5)

你的单身人士正在遵循Swift / Objective-C代码库中非常常见的模式。正如您所见,它也非常难以测试,也是编写不可测试代码的简便方法。有时单身人士是一个有用的模式,但我的经验是,模式的大多数用途实际上都不适合应用程序的需求。

来自Objective-C和Swift类常量单例的+shared_样式单例通常提供两种行为:

  1. 可能会强制只能实例化一个类的单个实例。 (实际上,这通常不会强制执行,您可以继续alloc / init其他实例,而应用程序依赖于开发人员遵循通过类方法专门访问共享实例的约定。)
  2. 它充当全局,允许访问类的共享实例。
  3. 行为#1偶尔会有用,而行为#2只是具有设计模式文凭的全球行为。

    我会通过完全删除全局变量来解决你的冲突。一直注入您的依赖项,而不仅仅是用于测试,并考虑当您需要某些东西来协调您注入的任何共享资源集时,应用程序中承担的责任。

    在整个应用程序中注入依赖关系的第一步通常是痛苦的; "但我到处都需要这个实例!​​"。使用它作为重新考虑设计的提示,为什么这么多组件访问相同的全局状态以及如何建模以提供更好的隔离?

    在某些情况下,您需要某个可变共享状态的单个副本,而单例实例可能是最佳实现。但是我发现在大多数例子中仍然没有成立。开发人员通常会寻找共享状态但有一些条件:在连接外部显示器之前只有一个屏幕,只有一个用户才会注销并进入第二个帐户,其中只有一个网络请求队列,直到您发现需要经过身份验证和匿名请求。类似地,在执行下一个测试用例之前,您经常需要共享实例。

    考虑到单身"似乎很少使用可用的初始化器(或返回现有共享实例的obj-c初始化方法),开发人员很乐意按惯例共享此状态,所以我看不到理由不是注入共享对象并编写容易测试的类而不是使用全局变量。

答案 1 :(得分:0)

我最终使用代码

解决了这个问题
class SingletonClass: NSObject {

    #if TEST

    // Only used for tests
    static var sharedInstance: SingletonClass!

    // Public init
    override init() {
        super.init()
    }

    #else

    // Only accessible using singleton
    static let sharedInstance = SingletonClass()

    private override init() {
        super.init()
    }

    #endif

    func callMethod() {
        // Do something crazy that shouldn't run under tests
    }

}

这样我可以在测试期间轻松模拟我的课程:

private class MockSingleton : SingletonClass {
    override callMethod() {}
}

在测试中:

SingletonClass.sharedInstance = MockSingleton()

通过将-D TEST添加到"其他Swift Flags"来激活仅测试代码。在应用测试目标的构建设置中。

答案 2 :(得分:0)

我的应用程序中存在类似的问题,在我的情况下,将Singletons用于这些特定服务是有意义的,因为它们是外部服务的代理,也是单身人士。

我最终使用https://github.com/Swinject/Swinject实现了依赖注入模型。花了大约一天的时间来实现大约6个类,这似乎是很多时间来实现这种级别的单元可测试性。它确实让我思考了我的服务类之间的依赖关系,它让我在类定义中更明确地指出了这些。我使用Swinject中的ObjectScope向DI框架表明他们是单身人士:https://github.com/Swinject/Swinject/blob/master/Documentation/ObjectScopes.md

我能够拥有我的单身人士,并将他们的模拟版本传递给我的单元测试。

关于这种方法的想法:它似乎更容易出错,因为我可能忘记正确初始化我的依赖项(并且永远不会收到运行时错误)。最后,它不会阻止某人直接实例化我的Service类的实例(这是单例的整点),因为我的init方法必须公开,以便DI Framework实例化对象进入注册表。