This is a question that came up in my mentorship program Slack team. I think it’s worth a blog post.
I’m trying to set up some tests according to the templates and the new book. I’m struggling with one small detail. In the book, you set up an
ordersWorkerSpy
, which is derived from a normal class. My question is: What to do if that class is a singleton, which is accessed through something likeordersWorkerSpy.shared
?
Let’s work through a code example.
The test subject
Let’s define a view controller that depends on a worker. You’ve seen plenty of examples where the interactors hand off the work to one or more workers. But the worker concept applies equally to presenters and view controllers too. Even a worker can have workers. It’s just a way of delegating complex work to other, smaller components. Breaking up a large class into multiple, smaller classes.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class ViewController: UIViewController { var worker = Worker.sharedWorker override func viewDidLoad() { super.viewDidLoad() doSomeWork() } func doSomeWork() { worker.doSomething() } } |
The ViewController
class has an instance variable worker
that is initialized to the Worker
singleton. The viewDidLoad()
method calls doSomeWork()
which, in turn, calls doSomething()
on the worker
object. Pretty typical.
The dependency
In the Worker
class, the singleton mechanism is done by defining a static constant sharedWorker
, and making the default init()
private. This has become the simplest, Swiftest way of creating a singleton class.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
import Foundation class Worker { static let sharedWorker = Worker() init() {} // private init() {} func doSomething() { debugPrint("Doing something in the app code") } } |
This works perfectly in the app code. However, in the test target, when we define the WorkerSpy
to inherit from Worker
, we have to remove the private
keyword so that we can override it. If not, the compiler will complain. This is why that line is commented out above. Yes, it’s a tradeoff that we make between enforceability and simplicity. But it’s a small sacrifice in order to keep our test code straightforward to write, as you’ll see next.
The test double
Now we can create the test double in the usual, familiar way.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class WorkerSpy: Worker { static let sharedWorkerSpy = WorkerSpy() override init() {} var doSomethingCalled = false override func doSomething() { debugPrint("Doing something in the test code") doSomethingCalled = true } } |
We make the WorkerSpy
inherit from Worker
. And then define the sharedWorkerSpy
static constant in the same way we did for the test subject. Now, we can override init()
to do nothing. The rest is the usual stuff. Override the doSomething()
method and record the method invocation.
The test method
Finally, here is the code for the complete test class. But I’m just going to focus on talking about the test method. You can read about how this view controller test is structured and set up in this post.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
class ViewControllerTests: XCTestCase { // MARK: - Subject under test var sut: ViewController! var window: UIWindow! // MARK: - Test lifecycle override func setUp() { super.setUp() window = UIWindow() setupViewController() } override func tearDown() { window = nil super.tearDown() } // MARK: - Test setup func setupViewController() { let bundle = Bundle.main let storyboard = UIStoryboard(name: "Main", bundle: bundle) sut = storyboard.instantiateViewController(withIdentifier: "ViewController") as! ViewController } func loadView() { window.addSubview(sut.view) RunLoop.current.run(until: Date()) } func testDoSomeWork() { // Given let workerSpy = WorkerSpy.sharedWorkerSpy sut.worker = workerSpy // When sut.doSomeWork() // Then XCTAssertTrue(workerSpy.doSomethingCalled, "doSomeWork() should invoke doSomething()") } } |
In the testDoSomeWork()
test method, we use the WorkerSpy
singleton instead of the Worker
singleton – sut.worker = workerSpy
. When the test executes, the doSomething()
method in the spy will be invoked instead of the real one.
As you just saw, singletons are nothing special in terms of unit testing.
Want to learn more about writing unit tests for your app? Check out my new book – Effective Unit Testing.
By setting init() as non-private class becomes non-singleton,
nothing stops developer to use it (init) directly in the code later on.
This is fine but what happens if your Worker is used by multiple classes and you want to design a test abstracted around a general use case ? Most often the shared instance of the singleton is being used globally as opposed to being declared as a type variable.
Also why would you write the { on a new line ? 😀
Sorry but this approach has a little to do with singleton. It’s typical case of dependency injection, which is not commonly used on iOS platform. The question about mocking a global singleton still remains not answered.