Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mocking singleton/sharedInstance in Swift

I have a class that I want to test using XCTest, and this class looks something like this:

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

The class uses a singleton that is implemented as this:

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

Now for the test. I want to test that method() actually does what it is supposed to do, but I don't want to invoke the code in callMethod() (because it does some horrible async/network/thread stuff that shouldn't run under tests of MyClass, and will make the tests crash).

So what I basically would like to do is this:

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

This obviously isn't valid Swift, but you get the idea. How can I override callMethod() just for this particular test, to make it harmless?

EDIT: I tried solving this using a form of dependency injection, but ran into big problems. I created a custom init-method just to be used for tests such that I could create my objects like this:

let myObject = MyClass(singleton: MockSingletonClass)

and let MyClass look like this

public class MyClass: NSObject {
    let singleton: SingletonClass

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

    init() {
        singleton = SingletonClass.sharedInstance
    }

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

Mixing in test code with the rest of the code is something I find a bit unpleasing, but okay. The BIG problem was that I had two singletons constructed like this in my project, both referencing each other:

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

This created a deadlock when one of the singletons where first used, as the init method would wait for the init method of the other, and so on...

like image 572
Jambaman Avatar asked Jan 29 '16 21:01

Jambaman


People also ask

Can a singleton class be mocked?

There is a way to mock Singleton. Use powermock to mock static method and use Whitebox to invoke constructor YourClass mockHelper = Whitebox . invokeConstructor(YourClass.

Can Mockito mock singleton?

If you are using Mockito 3.4. 0+, you may mock a singleton like the following, without PowerMock or other dependencies.

What is the advantage of singleton design pattern in Swift?

The main advantage of the singleton pattern is its ease of access. Wherever you need the shared instance, you can access it without modifying your code further. It's also possible to enforce a single instance if you make the Facade class's initializer private.

What is singleton class in swift with example?

In Swift, Singleton is a design pattern that ensures a class can have only one object. Such a class is called singleton class. An initializer allows us to instantiate an object of a class. And, making the initializer of a class restricts the object creation of the class from outside of the class.


2 Answers

Your singletons are following a very common pattern in Swift/Objective-C code bases. It is also, as you have seen, very difficult to test and an easy way to write untestable code. There are times when a singleton is a useful pattern but my experience has been that most uses of the pattern are actually a poor fit for the needs of the app.

The +shared_ style singleton from Objective-C and Swift class constant singletons usually provide two behaviors:

  1. It might enforce that only a single instance of a class can be instantiated. (In practice this is often not enforced and you can continue to alloc/init additional instances and the app instead depends on developers following a convention of exclusively accessing a shared instance via the class method.)
  2. It acts as a global, allowing access to a shared instance of a class.

Behavior #1 is occasionally useful while behavior #2 is just a global with a design pattern diploma.

I would resolve your conflict by removing the globals entirely. Inject your dependencies all of the time instead of just for testing and consider what responsibility that exposes in your app when you need something to coordinate whatever set of shared resources you're injecting.

A first pass at injecting dependencies throughout an app is often painful; "but I need this instance everywhere!". Use it as a prompt to reconsider the design, why are so many components accessing the same global state and how might it be modeled instead to provide better isolation?


There are cases where you want a single copy of some mutable shared state and a singleton instance is perhaps the best implementation. However I find that in most examples that still doesn't hold true. Developers are usually looking for shared state but with some conditions: there's only one screen until an external display is connected, there's only one user until they sign out and into a second account, there's only one network request queue until you find a need for authenticated vs anonymous requests. Similarly you often want a shared instance until the execution of the next test case.

Given how few "singleton"s seem to use failable initializers (or obj-c init methods which return an existing shared instance) it seems that developers are happy to share this state by convention so I see no reason not to inject the shared object and write readily testable classes instead of using globals.

like image 129
Jonah Avatar answered Oct 05 '22 10:10

Jonah


I eventually solved this using the code

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
    }

}

This way I can easily mock my class during tests:

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

In tests:

SingletonClass.sharedInstance = MockSingleton()

The test-only code is activated by adding -D TEST to "Other Swift Flags" in Build Settings for the app test target.

like image 25
Jambaman Avatar answered Oct 05 '22 12:10

Jambaman