在Swift中模拟singleton/sharedInstance

Jam*_*man 5 singleton mocking xctest swift

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

public class MyClass: NSObject {
    func method() {
         // Do something...
         // ...
         SingletonClass.sharedInstance.callMethod()
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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
    }
}
Run Code Online (Sandbox Code Playgroud)

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

所以我基本上想做的是:

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

这显然是无效的Swift,但你明白了.我怎样才能覆盖callMethod()这个特定的测试,使其无害?

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

let myObject = MyClass(singleton: MockSingletonClass)
Run Code Online (Sandbox Code Playgroud)

让MyClass看起来像这样

public class MyClass: NSObject {
    let singleton: SingletonClass

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

    init() {
        singleton = SingletonClass.sharedInstance
    }

    func method() {
         // Do something...
         // ...
         singleton.callMethod()
    }
}
Run Code Online (Sandbox Code Playgroud)

将测试代码与其余代码混合是我觉得有点不愉快,但没关系.BIG的问题是我在我的项目中有两个像这样构建的单身人士,他们互相引用:

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
    }
}
Run Code Online (Sandbox Code Playgroud)

当第一个使用的单例之一,init方法等待另一个的init方法时,这会造成死锁,依此类推......

Jon*_*nah 6

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

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

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

行为#1偶尔会有用,而行为#2只是具有设计模式文凭的全球性行为.

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

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


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

鉴于"单身"似乎很少使用可用的初始化器(或返回现有共享实例的obj-c初始化方法),似乎开发人员很乐意按惯例共享此状态,因此我认为没有理由不注入共享对象和写容易测试的类而不是使用全局变量.


Jam*_*man 1

我最终使用代码解决了这个问题

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
    }

}
Run Code Online (Sandbox Code Playgroud)

这样我就可以在测试期间轻松模拟我的班级:

private class MockSingleton : SingletonClass {
    override callMethod() {}
}
Run Code Online (Sandbox Code Playgroud)

在测试中:

SingletonClass.sharedInstance = MockSingleton()
Run Code Online (Sandbox Code Playgroud)

-D TEST通过添加到应用程序测试目标的“构建设置”中的“其他 Swift 标志”来激活仅测试代码。

  • 让测试泄漏到生产代码中的糟糕解决方案 (27认同)