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...
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:
- 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.)
- 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.
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.
I had a similar issue in my app, and in my case it made sense to use Singletons for these particular services as they were proxies for external services that were also singletons.
I ended up implementing a Dependency Injection model using https://github.com/Swinject/Swinject. It took about a day to implement for about 6 classes which seems like a lot of time to enable this level of unit testability. It did make me think hard about the dependencies between my service classes, and it made me more explicitly indicate these in the class definitions. I used the ObjectScope in Swinject to indicate to the DI framework that they're singletons: https://github.com/Swinject/Swinject/blob/master/Documentation/ObjectScopes.md
I was able to have my singletons, and pass in mock versions of them to my unit tests.
Thoughts on this approach: it seems more error prone as I could forget to initialize my dependencies correctly (and would never receive a runtime error). Finally, it doesn't prevent someone from just instantiating a instance of my Service class directly (which was sort of the whole point of the singleton), since my init methods had to be made public for the DI Framework to instantiate the objects into the registry.