One of the main issues we are facing when we try to make an old piece of code testable is dealing with static methods. Likely the are ways to make our code testable. One of them we will explain in this article.
Let’s start with a simple example to demonstrate the issue.
We have a struct named “AModel” that has a method to return the a greeting. In this method there is a call to a static method.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
import Foundation struct AModel { private let name: String func getGreeting() -> String { return ACollaborator.createGreeting(name) } } import Foundation class ACollaborator { static func createGreeting(_ name: String) -> String { return "Good morning \(name)" } } |
How can we write a test for this?
There are two big issues with the above code. Firstly the AModel has an implicit dependency on ACollaborator. Secondly there is a call on static method that we need to control.
Lets’ address first the implicit dependency issue, by using constructor dependency injection and an interface.
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 |
import Foundation struct AModel { private let name: String private let aCollaborator: ACollaboratorProtocol init(name: String, aCollaborator: ACollaboratorProtocol) { self.name = name self.aCollaborator = aCollaborator } func getGreeting() -> String { return type(of: aCollaborator).createGreeting(name) } } import Foundation protocol ACollaboratorProtocol { static func createGreeting(_ name: String) -> String } class ACollaborator: ACollaboratorProtocol { static func createGreeting(_ name: String) -> String { return "Good morning \(name)" } } |
And now let’s get rid of the static method.
In this example is easy to just convert the static method to instance method
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 |
import Foundation struct AModel { private let name: String private let aCollaborator: ACollaboratorProtocol init(name: String, aCollaborator: ACollaboratorProtocol) { self.name = name self.aCollaborator = aCollaborator } func getGreeting() -> String { return aCollaborator.createGreeting(name) } } import Foundation protocol ACollaboratorProtocol { func createGreeting(_ name: String) -> String } class ACollaborator { func createGreeting(_ name: String) -> String { return "Good morning \(name)" } } |
But let assume that is difficult to convert the static method to instance method. Then we can use the following approach
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 |
import Foundation struct AModel { private let name: String private let aCollaborator: ACollaboratorProtocol init(name: String, aCollaborator: ACollaboratorProtocol) { self.name = name self.aCollaborator = aCollaborator } func getGreeting() -> String { return aCollaborator.createGreeting(name) } } import Foundation protocol ACollaboratorProtocol { func createGreeting(_ name: String) -> String } class TesatbleACollaborator: ACollaboratorProtocol { func createGreeting(_ name: String) -> String { ACollaborator.createGreeting(name) } } class ACollaborator { static func createGreeting(_ name: String) -> String { return "Good morning \(name)" } } |
And now we can write our tests:
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 |
import XCTest @testable import StaticMethodOnCollaborator class AModelTests: XCTestCase { func test_getGreeting_returns_correct_string() { let sut = AModel(name: "Fragi", aCollaborator: TesatbleACollaborator()) let greeting = sut.getGreeting() XCTAssertEqual("Good morning Fragi", greeting) } func test_getGreeting_callsACollaborator() { let mockTestableCollaborator = MockTestableCollaborator() let sut = AModel(name: "Fragi", aCollaborator: mockTestableCollaborator) _ = sut.getGreeting() XCTAssertEqual(1, mockTestableCollaborator.didCallCreateGreeting.count) XCTAssertEqual("Fragi", mockTestableCollaborator.didCallCreateGreeting.first) } private class MockTestableCollaborator: ACollaboratorProtocol { private(set) var didCallCreateGreeting: [String] = [] func createGreeting(_ name: String) -> String { didCallCreateGreeting.append(name) return "" } } } |