02 Jun 2021
swift - unit test singletons
How to Unit Test Singletons
-
Sure singletons are an anti-pattern and not usually recommended, but sometimes we have a dependency on a singleton (e.g. Firebase). Therefore we need a way to unit test it, so this blog shows you how to do so!
-
Link to repo
Steps:
- Create a protocol with the methods that are dependencies, which needed to be faked out
- Create a wrapper around the singleton class that implements this protocol. It is a passthrough class
- Dependency inject the singleton in your consumer with the type as the protocol and a default parameter of the wrapper
- Create a fake that implements the protocol so you can dependency inject this fake in order to test the consumer of the singleton
Singleton Example
import Foundation
/// This is an example of a singleton that's a black box.
/// Pretend it comes from a third party library and we have no insight into what it does
class Singleton {
class func run(with parameter: String) -> String {
let run = "Singleton: Does something epic with: \(parameter)"
print(run)
return run
}
}
Singleton Wrapper
/// Create a protocol for all the public apis that we use for that singleton
protocol SingletonProtocol {
func run(with parameter: String) -> String
}
/// A Wrapper around the Singleton
/// Need to create a wrapper around this singleton so that we can implement a protcol, which will be used to create our fake
/// This wrapper is a pass through and implements the protocol
class SingletonWrapper: SingletonProtocol {
func run(with parameter: String) -> String {
return Singleton.run(with: parameter)
}
}
/// A sample class that has a dependency on a singleton
/// We will dependency inject the singleton and its type will be a protocol
/// This is the class that we will unit test
class SingletonConsumer {
private var singleton: SingletonProtocol!
var animal: String?
init(singleton: SingletonProtocol=SingletonWrapper()) {
self.singleton = singleton
}
func doSomethingCool(with parameter: String) {
let value = singleton.run(with: parameter)
if value.contains(Animal.cats.rawValue) {
animal = Animal.cats.rawValue
} else if value.contains(Animal.dogs.rawValue) {
animal = Animal.dogs.rawValue
} else if value.contains(Animal.elephants.rawValue) {
animal = Animal.elephants.rawValue
}
}
}
enum Animal: String {
case cats
case dogs
case elephants
}
Testing Code
class FakeSingleton: SingletonProtocol {
func run(with parameter: String) -> String {
return parameter
}
}
/// Tests`SingletonConsumer`
class SingletonExampleTests: XCTestCase {
func test_doSomethingCoolWithNonMatchingAnimalValues_setsAnimal_nil() {
let singletonConsumer = SingletonConsumer(singleton: FakeSingleton())
singletonConsumer.doSomethingCool(with: "spiders")
let actual = singletonConsumer.animal
XCTAssertNil(actual)
}
func test_doSomethingCoolWithCats_setsAnimalToCats() {
let singletonConsumer = SingletonConsumer(singleton: FakeSingleton())
singletonConsumer.doSomethingCool(with: "cats")
let actual = singletonConsumer.animal
let expected = Animal.cats.rawValue
XCTAssertEqual(actual, expected)
}
}
Til next time,
lovelejess
at 21:25